@vibe-cafe/vibe-usage 0.10.24 → 0.10.25

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,31 +4,37 @@ Track your AI coding tool token usage and sync to [vibecafe.ai](https://vibecafe
4
4
 
5
5
  ## Quick Start
6
6
 
7
+ One command, nothing to configure:
8
+
7
9
  ```bash
8
10
  npx @vibe-cafe/vibe-usage
9
11
  ```
10
12
 
11
- That's it. The CLI opens [vibecafe.ai/usage/device](https://vibecafe.ai/usage/device) in your browser; sign in, confirm the verification code shown in your terminal, click 「确认链接」, and the CLI receives an API key automatically.
13
+ On the first run it:
14
+ 1. Opens [vibecafe.ai/usage/device](https://vibecafe.ai/usage/device) in your browser — sign in, confirm the code shown in the terminal, click 「确认链接」; the key is saved to `~/.vibe-usage/config.json`
15
+ 2. Detects the AI coding tools installed on this machine
16
+ 3. Uploads your usage history
17
+ 4. Turns on background sync (every 30 minutes, starts at login) — no prompt, nothing else to install
18
+
19
+ Run the same command again any time to sync right now. To turn background sync off: `npx @vibe-cafe/vibe-usage daemon uninstall`. Add `--no-daemon` to the first run if you don't want the background service at all.
12
20
 
13
- After approval, it will:
14
- 1. Save your API key to `~/.vibe-usage/config.json`
15
- 2. Detect installed AI coding tools
16
- 3. Run an initial sync of your usage data
17
- 4. Prompt you to enable the background daemon for continuous syncing (recommended)
21
+ Prefer a menu-bar app? [Vibe Usage for Mac](https://github.com/vibe-cafe/vibe-usage-app) · [for Windows](https://github.com/vibe-cafe/vibe-usage-windows).
18
22
 
19
23
  ### CI / Headless
20
24
 
21
- If you don't have a local browser (CI, remote SSH session, container), pre-issue a key at [vibecafe.ai/usage/setup](https://vibecafe.ai/usage/setup) and pass it on the command line:
25
+ If you don't have a local browser (CI, remote SSH session, container), pre-issue a key at [vibecafe.ai/usage/setup](https://vibecafe.ai/usage/setup) and pass it on the command line. Non-interactive runs never install the background service:
22
26
 
23
27
  ```bash
24
- npx @vibe-cafe/vibe-usage init --manual-key vbu_xxxxxxxxxxxx
28
+ npx @vibe-cafe/vibe-usage init --manual-key vbu_xxxxxxxxxxxx --no-daemon
25
29
  ```
26
30
 
27
- ## Commands
31
+ <details>
32
+ <summary><strong>All commands</strong> — everything below still works; the older spellings print a hint pointing at the simpler form</summary>
28
33
 
29
34
  ```bash
30
- npx @vibe-cafe/vibe-usage # Init (first run, browser login) or sync (subsequent runs)
31
- npx @vibe-cafe/vibe-usage init # Re-run setup via browser login
35
+ npx @vibe-cafe/vibe-usage # Init (first run, browser login, then background sync) or sync (subsequent runs)
36
+ npx @vibe-cafe/vibe-usage --no-daemon # Same, but skip installing the background service on first run
37
+ npx @vibe-cafe/vibe-usage init # Re-run setup via browser login (also how you re-bind to another account)
32
38
  npx @vibe-cafe/vibe-usage init --manual-key <vbu_...> # Skip browser, use pre-issued key (CI/headless)
33
39
  npx @vibe-cafe/vibe-usage sync # Manual sync
34
40
  npx @vibe-cafe/vibe-usage sync --extra-codex-home /path/to/.codex # Add another Codex Home for this run only
@@ -45,8 +51,11 @@ npx @vibe-cafe/vibe-usage reset --local # Delete this host's data only and re-u
45
51
  npx @vibe-cafe/vibe-usage skill # Install skill for AI coding assistants
46
52
  npx @vibe-cafe/vibe-usage skill --remove # Remove installed skills
47
53
  npx @vibe-cafe/vibe-usage status # Show config & detected tools
54
+ npx @vibe-cafe/vibe-usage help --all # Full help (plain `help` shows the short version)
48
55
  ```
49
56
 
57
+ </details>
58
+
50
59
  ## Supported Tools
51
60
 
52
61
  | Tool | Data Location |
@@ -93,7 +102,7 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
93
102
  - Incremental Codex parsing: a versioned, disposable cache under `~/.vibe-usage/cache/codex/` stores per-rollout aggregate results and parser continuation state. Unchanged rollouts require no raw-log reads; an ordinary append reads only the new tail; forks, sub-agents, replacements, truncations, and failed safety checks fall back to the full correctness path. A bounded rolling audit occasionally re-reads one historical file. Very large first-time indexes checkpoint before the Mac app timeout and resume on the next sync instead of restarting
94
103
  - The Codex parser cache contains derived aggregates and replay metadata, not raw prompt or response text. It is independent of upload state and can be deleted safely (the next sync rebuilds it). `reset` intentionally keeps it so the required full re-upload does not also require a full disk rescan. Set `VIBE_USAGE_CODEX_CACHE=0` to disable the optimization for diagnosis
95
104
  - SQLite-backed tools are read via Node's built-in `node:sqlite` on Node ≥ 22.5 — no `sqlite3` binary needed (works on Windows out of the box); on older Node the CLI falls back to the system `sqlite3` executable
96
- - For continuous syncing, use `npx @vibe-cafe/vibe-usage daemon` or the [Vibe Usage Mac app](https://github.com/vibe-cafe/vibe-usage-app)
105
+ - Continuous syncing is on by default: the first run installs a background service (see [Background sync](#background-sync)); the [Vibe Usage Mac app](https://github.com/vibe-cafe/vibe-usage-app) is the menu-bar alternative
97
106
 
98
107
  ## Cursor 网络排查
99
108
 
@@ -211,36 +220,30 @@ npx @vibe-cafe/vibe-usage config remove-root grok /path/to/grok-home
211
220
 
212
221
  Default roots are always scanned and existing `codexExtraHome` configurations remain valid. Additional roots are only scanned after they are explicitly added. If a configured root later becomes unavailable, that tool is skipped for the current sync so its incremental upload state is not pruned.
213
222
 
214
- ## Daemon Mode
215
-
216
- ### Background service (recommended)
223
+ ## Background sync
217
224
 
218
- Install as a system service for automatic background syncing:
225
+ The first `npx @vibe-cafe/vibe-usage` run installs a user-level service (systemd on Linux, launchd on macOS, Task Scheduler on Windows — no admin rights needed) that syncs every 30 minutes and starts automatically on login. Nothing else to do.
219
226
 
220
- ```bash
221
- npx @vibe-cafe/vibe-usage daemon install
222
- ```
223
-
224
- This creates a user-level service (systemd on Linux, launchd on macOS, Task Scheduler on Windows — no admin rights needed) that syncs every 30 minutes and starts automatically on login. Manage with:
227
+ <details>
228
+ <summary>Managing the service, and how it is launched</summary>
225
229
 
226
230
  ```bash
227
231
  npx @vibe-cafe/vibe-usage daemon status
228
232
  npx @vibe-cafe/vibe-usage daemon stop
229
233
  npx @vibe-cafe/vibe-usage daemon restart
230
234
  npx @vibe-cafe/vibe-usage daemon uninstall
235
+ npx @vibe-cafe/vibe-usage daemon install # only needed after --no-daemon or uninstall
231
236
  ```
232
237
 
233
- For reliable operation, install globally first: `npm install -g @vibe-cafe/vibe-usage`
234
-
235
- ### Foreground mode
238
+ **How the service starts the CLI.** When you ran the CLI through `npx`, the service is registered as `npx --yes @vibe-cafe/vibe-usage@latest daemon`, so it survives `npm cache clean` and picks up the newest release at every login. When the CLI was installed globally (`npm install -g @vibe-cafe/vibe-usage`) or run from a checkout, the service pins that exact `node <bin> daemon` path instead — upgrade the package and `daemon restart` to pick up a new version. `daemon status` prints which of the two forms a machine has. A service installed by a CLI older than 0.10.25 keeps its pinned npx-cache path; to switch it to the self-updating form run `daemon uninstall` and then the bare command once.
236
239
 
237
- Run continuous syncing in the foreground (every 30 minutes):
240
+ **Foreground mode** (no service, Ctrl+C to stop):
238
241
 
239
242
  ```bash
240
243
  npx @vibe-cafe/vibe-usage daemon
241
244
  ```
242
245
 
243
- Press Ctrl+C to stop.
246
+ </details>
244
247
 
245
248
  ## License
246
249
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.10.24",
3
+ "version": "0.10.25",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,12 +1,16 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { writeFileSync, readFileSync, unlinkSync, mkdirSync, existsSync } from 'node:fs';
3
- import { join } from 'node:path';
3
+ import { join, dirname, win32 as winPath } from 'node:path';
4
4
  import { homedir, platform } from 'node:os';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { success, failure, warn, dim } from './output.js';
7
7
 
8
8
  const SERVICE_NAME = 'vibe-usage';
9
9
  const LAUNCHD_LABEL = 'ai.vibecafe.vibe-usage';
10
+ // Same specifier the Mac app resolves on every sync (vibe-usage-app
11
+ // RuntimeDetector.swift): a service that runs this instead of a pinned bin
12
+ // path survives npx cache cleanup and picks up new releases at each login.
13
+ export const PACKAGE_SPEC = '@vibe-cafe/vibe-usage@latest';
10
14
 
11
15
  function detectPlatform() {
12
16
  const os = platform();
@@ -23,16 +27,45 @@ function detectPlatform() {
23
27
  return null;
24
28
  }
25
29
 
30
+ // npx cache paths are unstable — a service pinned to one breaks when the
31
+ // cache is cleared. POSIX: ~/.npm/_npx/<hash>/...; Windows:
32
+ // %LocalAppData%\npm-cache\_npx\...
33
+ export function isNpxCachePath(binPath) {
34
+ return /[\\/]_npx[\\/]/.test(binPath);
35
+ }
36
+
37
+ /**
38
+ * When the CLI itself came from the npx cache, the service should not pin
39
+ * that path; it should re-resolve the package through npx at every start.
40
+ * Returns null when no npx lives next to the running node (then the caller
41
+ * falls back to pinning the path and warning, as before).
42
+ */
43
+ export function npxLauncher(nodePath, exists = existsSync, os = platform()) {
44
+ const nodeDir = dirname(nodePath);
45
+ const npxPath = join(nodeDir, os === 'win32' ? 'npx.cmd' : 'npx');
46
+ return exists(npxPath) ? { mode: 'npx', npxPath, nodeDir } : null;
47
+ }
48
+
26
49
  function resolvePaths() {
27
50
  const nodePath = process.execPath;
28
51
  const thisFile = fileURLToPath(import.meta.url);
29
52
  const binPath = join(thisFile, '..', '..', 'bin', 'vibe-usage.js');
53
+ const isNpxCache = isNpxCachePath(binPath);
54
+ const launcher = isNpxCache ? npxLauncher(nodePath) : null;
55
+ return { nodePath, binPath, isNpxCache, launcher };
56
+ }
30
57
 
31
- // npx cache paths are unstable service will break when cache is cleared.
32
- // POSIX: ~/.npm/_npx/<hash>/...; Windows: %LocalAppData%\npm-cache\_npx\...
33
- const isNpxCache = /[\\/]_npx[\\/]/.test(binPath);
58
+ // argv the service runs; the last token is always `daemon` so process
59
+ // matching and log greps keep working across both modes.
60
+ function serviceArgv(nodePath, binPath, launcher) {
61
+ if (launcher?.mode === 'npx') return [launcher.npxPath, '--yes', PACKAGE_SPEC, 'daemon'];
62
+ return [nodePath, binPath, 'daemon'];
63
+ }
34
64
 
35
- return { nodePath, binPath, isNpxCache };
65
+ // npx is a `#!/usr/bin/env node` script and launchd / systemd start services
66
+ // with a minimal PATH that has no node on it.
67
+ function servicePath(launcher) {
68
+ return [launcher.nodeDir, '/usr/local/bin', '/usr/bin', '/bin'].join(':');
36
69
  }
37
70
 
38
71
  function getServicePaths(plat) {
@@ -103,21 +136,26 @@ export function generateSystemdUnit(
103
136
  binPath,
104
137
  claudeConfigDir = process.env.CLAUDE_CONFIG_DIR?.trim(),
105
138
  env = process.env,
139
+ launcher = null,
106
140
  ) {
141
+ const npx = launcher?.mode === 'npx';
142
+ const pathLine = npx ? `Environment="PATH=${escapeSystemdEnvironment(servicePath(launcher))}"\n` : '';
107
143
  const environment = serviceEnvironment(claudeConfigDir, env)
108
144
  .map(([key, value]) => `Environment="${key}=${escapeSystemdEnvironment(value)}"\n`)
109
145
  .join('');
146
+ // npx mode needs the registry at start; RestartSec=60 keeps an offline boot
147
+ // from turning into a restart storm.
110
148
  return `[Unit]
111
149
  Description=VibeCafe Usage Tracker
112
150
  After=network.target
113
151
 
114
152
  [Service]
115
153
  Type=simple
116
- ExecStart=${nodePath} ${binPath} daemon
154
+ ExecStart=${serviceArgv(nodePath, binPath, launcher).join(' ')}
117
155
  Restart=on-failure
118
- RestartSec=10
156
+ RestartSec=${npx ? 60 : 10}
119
157
  Environment=NODE_ENV=production
120
- ${environment}WorkingDirectory=${homedir()}
158
+ ${pathLine}${environment}WorkingDirectory=${homedir()}
121
159
 
122
160
  [Install]
123
161
  WantedBy=default.target
@@ -129,11 +167,22 @@ export function generateLaunchdPlist(
129
167
  binPath,
130
168
  claudeConfigDir = process.env.CLAUDE_CONFIG_DIR?.trim(),
131
169
  env = process.env,
170
+ launcher = null,
132
171
  ) {
172
+ const npx = launcher?.mode === 'npx';
133
173
  const logDir = join(homedir(), '.vibe-usage');
174
+ const programArguments = serviceArgv(nodePath, binPath, launcher)
175
+ .map(arg => ` <string>${escapeXml(arg)}</string>\n`)
176
+ .join('');
177
+ const pathEntry = npx
178
+ ? ` <key>PATH</key>\n <string>${escapeXml(servicePath(launcher))}</string>\n`
179
+ : '';
134
180
  const environment = serviceEnvironment(claudeConfigDir, env)
135
181
  .map(([key, value]) => ` <key>${key}</key>\n <string>${escapeXml(value)}</string>\n`)
136
182
  .join('');
183
+ // ThrottleInterval only matters in npx mode: an offline boot makes npx exit
184
+ // non-zero and KeepAlive would otherwise relaunch it every 10 seconds.
185
+ const throttle = npx ? ` <key>ThrottleInterval</key>\n <integer>60</integer>\n` : '';
137
186
  return `<?xml version="1.0" encoding="UTF-8"?>
138
187
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
139
188
  <plist version="1.0">
@@ -142,15 +191,12 @@ export function generateLaunchdPlist(
142
191
  <string>${LAUNCHD_LABEL}</string>
143
192
  <key>ProgramArguments</key>
144
193
  <array>
145
- <string>${nodePath}</string>
146
- <string>${binPath}</string>
147
- <string>daemon</string>
148
- </array>
194
+ ${programArguments} </array>
149
195
  <key>RunAtLoad</key>
150
196
  <true/>
151
197
  <key>KeepAlive</key>
152
198
  <true/>
153
- <key>WorkingDirectory</key>
199
+ ${throttle} <key>WorkingDirectory</key>
154
200
  <string>${homedir()}</string>
155
201
  <key>StandardOutPath</key>
156
202
  <string>${join(logDir, 'daemon.log')}</string>
@@ -160,7 +206,7 @@ export function generateLaunchdPlist(
160
206
  <dict>
161
207
  <key>NODE_ENV</key>
162
208
  <string>production</string>
163
- ${environment} </dict>
209
+ ${pathEntry}${environment} </dict>
164
210
  </dict>
165
211
  </plist>
166
212
  `;
@@ -171,6 +217,7 @@ export function generateWindowsTaskCmd(
171
217
  binPath,
172
218
  claudeConfigDir = process.env.CLAUDE_CONFIG_DIR?.trim(),
173
219
  env = process.env,
220
+ launcher = null,
174
221
  ) {
175
222
  const logPath = join(homedir(), '.vibe-usage', 'daemon.log');
176
223
  const lines = [
@@ -178,12 +225,17 @@ export function generateWindowsTaskCmd(
178
225
  'rem Generated by `vibe-usage daemon install` - reinstalling overwrites this file',
179
226
  'set "NODE_ENV=production"',
180
227
  ];
228
+ if (launcher?.mode === 'npx') {
229
+ // npx.cmd re-launches node by name; the logon task's PATH may not have it.
230
+ lines.push(`set "PATH=${escapeCmdValue(launcher.nodeDir)};%PATH%"`);
231
+ }
181
232
  for (const [key, value] of serviceEnvironment(claudeConfigDir, env)) {
182
233
  lines.push(`set "${key}=${escapeCmdValue(value)}"`);
183
234
  }
184
- lines.push(
185
- `"${escapeCmdValue(nodePath)}" "${escapeCmdValue(binPath)}" daemon >> "${escapeCmdValue(logPath)}" 2>&1`,
186
- );
235
+ const invocation = launcher?.mode === 'npx'
236
+ ? `"${escapeCmdValue(launcher.npxPath)}" --yes ${PACKAGE_SPEC} daemon`
237
+ : `"${escapeCmdValue(nodePath)}" "${escapeCmdValue(binPath)}" daemon`;
238
+ lines.push(`${invocation} >> "${escapeCmdValue(logPath)}" 2>&1`);
187
239
  return lines.join('\r\n') + '\r\n';
188
240
  }
189
241
 
@@ -300,13 +352,26 @@ function windowsUserId() {
300
352
  // invocation. Process matching uses both paths so it works for Node and Bun
301
353
  // without touching foreground daemons from another checkout.
302
354
  export function parseWindowsTaskInvocation(cmd) {
303
- const match = cmd.match(/^"([^"]+)" "([^"]+)" daemon\b/m);
304
- if (!match) return null;
305
355
  const unescapeCmdValue = value => value.replace(/%%/g, '%');
306
- return {
307
- runtimePath: unescapeCmdValue(match[1]),
308
- binPath: unescapeCmdValue(match[2]),
309
- };
356
+ const pinned = cmd.match(/^"([^"]+)" "([^"]+)" daemon\b/m);
357
+ if (pinned) {
358
+ return {
359
+ runtimePath: unescapeCmdValue(pinned[1]),
360
+ binPath: unescapeCmdValue(pinned[2]),
361
+ };
362
+ }
363
+ // npx mode: the live daemon is node.exe (next to npx.cmd) running the
364
+ // cached bin, whose path we cannot know in advance — match on the bin name.
365
+ const npx = cmd.match(/^"([^"]+)" --yes @vibe-cafe\/vibe-usage@\S+ daemon\b/m);
366
+ if (npx) {
367
+ const npxPath = unescapeCmdValue(npx[1]);
368
+ return {
369
+ runtimePath: winPath.join(winPath.dirname(npxPath), 'node.exe'),
370
+ binPath: 'vibe-usage.js',
371
+ mode: 'npx',
372
+ };
373
+ }
374
+ return null;
310
375
  }
311
376
 
312
377
  function readTaskInvocation(paths) {
@@ -336,6 +401,36 @@ function daemonProcessKillLines(invocation) {
336
401
  : [];
337
402
  }
338
403
 
404
+ export function isDaemonPlatform() {
405
+ return detectPlatform() !== null;
406
+ }
407
+
408
+ // A leftover daemon-task.xml without a registration (crashed uninstall) must
409
+ // not block a fresh install, so Task Scheduler checks the live task instead.
410
+ export function isDaemonInstalled() {
411
+ const plat = detectPlatform();
412
+ if (!plat) return false;
413
+ const paths = getServicePaths(plat);
414
+ return plat === 'taskscheduler' ? taskExists() : existsSync(paths.file);
415
+ }
416
+
417
+ // How an installed service starts the daemon, read back from the unit itself
418
+ // so `status` tells the truth about machines set up by older versions. The
419
+ // plist splits argv into separate <string> elements, so look for the package
420
+ // spec alone: a pinned bin path contains `@vibe-cafe/vibe-usage/bin`, never
421
+ // `@latest`.
422
+ export function installedModeFromText(text) {
423
+ return text.includes(PACKAGE_SPEC) ? 'npx' : 'pinned';
424
+ }
425
+
426
+ export function describeInstalledMode(plat, paths) {
427
+ try {
428
+ return installedModeFromText(readFileSync(plat === 'taskscheduler' ? paths.cmd : paths.file, 'utf-8'));
429
+ } catch {
430
+ return null;
431
+ }
432
+ }
433
+
339
434
  function install() {
340
435
  const plat = detectPlatform();
341
436
  if (!plat) {
@@ -344,20 +439,17 @@ function install() {
344
439
  return;
345
440
  }
346
441
 
347
- const { nodePath, binPath, isNpxCache } = resolvePaths();
442
+ const { nodePath, binPath, isNpxCache, launcher } = resolvePaths();
348
443
 
349
- if (isNpxCache) {
350
- console.log(warn('检测到从 npx 缓存运行 vibe-usage,缓存清理后 daemon 会失效。'));
444
+ if (isNpxCache && !launcher) {
445
+ console.log(warn('检测到从 npx 缓存运行 vibe-usage 且找不到 npx,缓存清理后 daemon 会失效。'));
351
446
  console.log(dim(' 建议先全局安装: npm install -g @vibe-cafe/vibe-usage'));
352
447
  console.log();
353
448
  }
354
449
 
355
450
  const paths = getServicePaths(plat);
356
451
 
357
- // A leftover daemon-task.xml without a registration (crashed uninstall) must
358
- // not block a fresh install, so Task Scheduler checks the live task instead
359
- const alreadyInstalled = plat === 'taskscheduler' ? taskExists() : existsSync(paths.file);
360
- if (alreadyInstalled) {
452
+ if (isDaemonInstalled()) {
361
453
  console.log(warn('Daemon 已安装,运行 `vibe-usage daemon restart` 或 `uninstall` 先处理。'));
362
454
  return;
363
455
  }
@@ -365,7 +457,7 @@ function install() {
365
457
  mkdirSync(paths.dir, { recursive: true });
366
458
 
367
459
  if (plat === 'systemd') {
368
- writeFileSync(paths.file, generateSystemdUnit(nodePath, binPath), 'utf-8');
460
+ writeFileSync(paths.file, generateSystemdUnit(nodePath, binPath, undefined, process.env, launcher), 'utf-8');
369
461
  console.log(dim(` 已写入 ${paths.file}`));
370
462
 
371
463
  run('systemctl', ['--user', 'daemon-reload']);
@@ -379,7 +471,7 @@ function install() {
379
471
 
380
472
  if (plat === 'launchd') {
381
473
  mkdirSync(join(homedir(), '.vibe-usage'), { recursive: true });
382
- writeFileSync(paths.file, generateLaunchdPlist(nodePath, binPath), 'utf-8');
474
+ writeFileSync(paths.file, generateLaunchdPlist(nodePath, binPath, undefined, process.env, launcher), 'utf-8');
383
475
  console.log(dim(` 已写入 ${paths.file}`));
384
476
 
385
477
  const result = run('launchctl', ['load', paths.file]);
@@ -402,7 +494,7 @@ function install() {
402
494
  'wscript.exe',
403
495
  );
404
496
 
405
- writeFileSync(paths.cmd, generateWindowsTaskCmd(nodePath, binPath), 'utf-8');
497
+ writeFileSync(paths.cmd, generateWindowsTaskCmd(nodePath, binPath, undefined, process.env, launcher), 'utf-8');
406
498
  writeFileSync(paths.vbs, generateWindowsTaskVbs(paths.cmd), 'utf-8');
407
499
  writeFileSync(paths.file, generateWindowsTaskXml(userId, wscriptPath, paths.vbs), 'utf-8');
408
500
  console.log(dim(` 已写入 ${paths.file}`));
@@ -421,8 +513,11 @@ function install() {
421
513
  }
422
514
 
423
515
  console.log();
424
- console.log(success('Daemon 已安装,用量数据将每 30 分钟自动同步。'));
425
- console.log(dim(' 运行 `vibe-usage daemon status` 查看状态。'));
516
+ console.log(success('已开启后台自动同步(每 30 分钟一次,登录自启)。'));
517
+ if (launcher?.mode === 'npx') {
518
+ console.log(dim(' 服务通过 npx 启动,每次登录自动使用最新版。'));
519
+ }
520
+ console.log(dim(' 关闭: npx @vibe-cafe/vibe-usage daemon uninstall'));
426
521
  }
427
522
 
428
523
  function uninstall() {
@@ -484,12 +579,19 @@ function status() {
484
579
 
485
580
  const paths = getServicePaths(plat);
486
581
 
582
+ const printMode = () => {
583
+ const mode = describeInstalledMode(plat, paths);
584
+ if (mode === 'npx') console.log(dim(` 运行方式: npx ${PACKAGE_SPEC}(每次登录自动更新)`));
585
+ else if (mode === 'pinned') console.log(dim(' 运行方式: 固定路径(升级后需 daemon uninstall 再重跑一条命令)'));
586
+ };
587
+
487
588
  if (plat === 'taskscheduler') {
488
589
  if (!taskExists()) {
489
590
  console.log(dim('未安装 daemon 服务。'));
490
591
  console.log(dim(' 运行 `vibe-usage daemon install` 安装。'));
491
592
  return;
492
593
  }
594
+ printMode();
493
595
  const invocation = readTaskInvocation(paths);
494
596
  const processExpression = windowsDaemonProcessExpression(invocation);
495
597
  const scriptLines = [
@@ -546,6 +648,7 @@ function status() {
546
648
  console.log(dim(' 运行 `vibe-usage daemon install` 安装。'));
547
649
  return;
548
650
  }
651
+ printMode();
549
652
 
550
653
  if (plat === 'systemd') {
551
654
  const result = run('systemctl', ['--user', 'status', `${SERVICE_NAME}.service`]);
package/src/daemon.js CHANGED
@@ -16,7 +16,7 @@ function sleep(ms) {
16
16
  export async function runDaemon({ codexExtraHome } = {}) {
17
17
  const config = loadConfig();
18
18
  if (!config?.apiKey) {
19
- console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage init`。'));
19
+ console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage`。'));
20
20
  process.exit(1);
21
21
  }
22
22
 
package/src/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  normalizeExtraRoot,
9
9
  validateExtraRoot,
10
10
  } from './extra-roots.js';
11
- import { dim as dimText, failure, smallHeader, warn } from './output.js';
11
+ import { dim as dimText, failure, hint, smallHeader, warn } from './output.js';
12
12
  import { fetchAccount } from './api.js';
13
13
 
14
14
  function printSmallHeader() {
@@ -23,7 +23,7 @@ async function showStatus() {
23
23
 
24
24
  if (!config?.apiKey) {
25
25
  console.log(' Config: not configured');
26
- console.log(` Run \`npx @vibe-cafe/vibe-usage init\` to set up.\n`);
26
+ console.log(` Run \`npx @vibe-cafe/vibe-usage\` to set up.\n`);
27
27
  } else {
28
28
  console.log(` Config: ${getConfigPath()}`);
29
29
  console.log(` API key: ${config.apiKey.slice(0, 8)}...`);
@@ -213,6 +213,69 @@ function extractOption(args, name) {
213
213
  return { args: [...args.slice(0, idx), ...args.slice(idx + 2)], value };
214
214
  }
215
215
 
216
+ // Boolean global flag: present anywhere in argv → true, removed from args.
217
+ function extractFlag(args, name) {
218
+ const flag = `--${name}`;
219
+ const idx = args.findIndex(a => a === flag);
220
+ if (idx === -1) return { args, value: false };
221
+ return { args: [...args.slice(0, idx), ...args.slice(idx + 1)], value: true };
222
+ }
223
+
224
+ const BARE = 'npx @vibe-cafe/vibe-usage';
225
+
226
+ // The one command we advertise. Everything else stays supported (see
227
+ // `help --all`) but the default help must not read like a matrix.
228
+ const SHORT_HELP = `
229
+ vibe-usage - Vibe Usage Tracker by VibeCafé
230
+
231
+ 用法:
232
+ ${BARE}
233
+ 首次运行: 浏览器登录 → 同步 → 自动开启后台同步(每 30 分钟一次)
234
+ 之后运行: 手动同步一次
235
+
236
+ 常用:
237
+ ${BARE} daemon status 查看后台同步
238
+ ${BARE} daemon uninstall 关闭后台同步
239
+ ${BARE} summary [--days N] 最近 N 天用量(默认 7)
240
+ ${BARE} help --all 全部命令与选项
241
+
242
+ 旧命令(sync、init、daemon install …)仍可用,见 help --all。
243
+ `;
244
+
245
+ const FULL_HELP = `
246
+ vibe-usage - Vibe Usage Tracker by VibeCafé
247
+
248
+ Usage:
249
+ ${BARE} Init (first run, browser login, then background sync) or sync
250
+ ${BARE} --no-daemon Same, but do not install the background service on first run
251
+ ${BARE} init Set up via browser login (default)
252
+ ${BARE} init --manual-key <vbu_...> Skip browser, use a pre-issued key (CI/headless)
253
+ ${BARE} sync Manually sync usage data
254
+ ${BARE} sync --extra-codex-home <path> Use another Codex Home for this run
255
+ ${BARE} summary Print last 7 days as markdown (cost/tokens/model/project)
256
+ ${BARE} summary --days N Same, but over the last N days (1-90)
257
+ ${BARE} daemon Continuous sync (every 30m, foreground)
258
+ ${BARE} daemon install Install background service (systemd/launchd/Task Scheduler)
259
+ ${BARE} daemon uninstall Remove background service
260
+ ${BARE} daemon status Show background service status
261
+ ${BARE} daemon stop Stop background service
262
+ ${BARE} daemon restart Restart background service
263
+ ${BARE} reset Delete all data and re-upload
264
+ ${BARE} reset --local Delete data for this host only and re-upload (--host is a legacy alias)
265
+ ${BARE} skill Install skill for AI coding tools
266
+ ${BARE} skill --remove Remove installed skills
267
+ ${BARE} status Show config and detected tools
268
+ ${BARE} config show Show full config as JSON
269
+ ${BARE} config get <key> Get a config value
270
+ ${BARE} config set <key> <value> Set a config value
271
+ ${BARE} config set codexExtraHome <path> Persist another Codex Home
272
+ ${BARE} config add-root <tool> <path> Add a Codex, Grok, Antigravity, or Pi data root
273
+ ${BARE} config remove-root <tool> <path> Remove an added data root
274
+ ${BARE} config roots Show added data roots as JSON
275
+ ${BARE} help Show the short help
276
+ ${BARE} help --all Show this full list
277
+ `;
278
+
216
279
  export async function run(rawArgs) {
217
280
  // --key and --manual-key both mean "skip device flow, take this vbu_ key".
218
281
  // --manual-key is the documented name; --key is kept as a legacy alias so
@@ -222,7 +285,10 @@ export async function run(rawArgs) {
222
285
  ({ args: stripped, value: apiKey } = extractOption(rawArgs, 'manual-key'));
223
286
  if (apiKey === undefined) {
224
287
  ({ args: stripped, value: apiKey } = extractOption(stripped, 'key'));
288
+ if (apiKey !== undefined) hint('--key 已改名 --manual-key,旧写法仍可用');
225
289
  }
290
+ let noDaemon;
291
+ ({ args: stripped, value: noDaemon } = extractFlag(stripped, 'no-daemon'));
226
292
  let codexExtraHome;
227
293
  ({ args: stripped, value: codexExtraHome } = extractOption(stripped, 'extra-codex-home'));
228
294
  if (codexExtraHome !== undefined) {
@@ -239,14 +305,19 @@ export async function run(rawArgs) {
239
305
 
240
306
  switch (command) {
241
307
  case 'init': {
308
+ // Re-running init on a configured machine is the account re-bind path;
309
+ // only a genuine first setup gets nudged toward the bare command.
310
+ const firstSetup = !loadConfig()?.apiKey;
242
311
  const { runInit } = await import('./init.js');
243
- await runInit({ apiKey, codexExtraHome });
312
+ await runInit({ apiKey, codexExtraHome, noDaemon });
313
+ if (firstSetup) hint(`以后直接运行 ${BARE} 即可:首次登录,之后同步`);
244
314
  break;
245
315
  }
246
316
  case 'sync': {
247
317
  printSmallHeader();
248
318
  const { runSync } = await import('./sync.js');
249
319
  await runSync({ codexExtraHome });
320
+ hint(`以后直接运行 ${BARE} 就是同步,不用再加 sync`);
250
321
  break;
251
322
  }
252
323
  case 'summary': {
@@ -256,15 +327,18 @@ export async function run(rawArgs) {
256
327
  }
257
328
  case 'reset': {
258
329
  printSmallHeader();
330
+ if (args.includes('--host')) hint('reset --host 已改名 reset --local,旧写法仍可用');
259
331
  const { runReset } = await import('./reset.js');
260
332
  await runReset(args.slice(1));
261
333
  break;
262
334
  }
263
335
  case 'daemon':
264
336
  case '--daemon': {
337
+ if (command === '--daemon') hint('--daemon 已改名 daemon,旧写法仍可用');
265
338
  const sub = args[1];
266
339
  if (sub === undefined) {
267
340
  // Foreground daemon loop — no header, just start syncing
341
+ hint(`首次运行 ${BARE} 会自动开启后台同步,不用手动跑 daemon`);
268
342
  const { runDaemon } = await import('./daemon.js');
269
343
  await runDaemon({ codexExtraHome });
270
344
  } else {
@@ -278,6 +352,7 @@ export async function run(rawArgs) {
278
352
  printSmallHeader();
279
353
  const { manageDaemon } = await import('./daemon-service.js');
280
354
  await manageDaemon(sub);
355
+ if (sub === 'install') hint(`首次运行 ${BARE} 会自动开启后台同步,不用单独装`);
281
356
  }
282
357
  break;
283
358
  }
@@ -298,37 +373,7 @@ export async function run(rawArgs) {
298
373
  case 'help':
299
374
  case '--help':
300
375
  case '-h': {
301
- console.log(`
302
- vibe-usage - Vibe Usage Tracker by VibeCafé
303
-
304
- Usage:
305
- npx @vibe-cafe/vibe-usage Init (first run, browser login) or sync
306
- npx @vibe-cafe/vibe-usage init Set up via browser login (default)
307
- npx @vibe-cafe/vibe-usage init --manual-key <vbu_...> Skip browser, use a pre-issued key (CI/headless)
308
- npx @vibe-cafe/vibe-usage sync Manually sync usage data
309
- npx @vibe-cafe/vibe-usage sync --extra-codex-home <path> Use another Codex Home for this run
310
- npx @vibe-cafe/vibe-usage summary Print last 7 days as markdown (cost/tokens/model/project)
311
- npx @vibe-cafe/vibe-usage summary --days N Same, but over the last N days (1-90)
312
- npx @vibe-cafe/vibe-usage daemon Continuous sync (every 30m, foreground)
313
- npx @vibe-cafe/vibe-usage daemon install Install background service (systemd/launchd/Task Scheduler)
314
- npx @vibe-cafe/vibe-usage daemon uninstall Remove background service
315
- npx @vibe-cafe/vibe-usage daemon status Show background service status
316
- npx @vibe-cafe/vibe-usage daemon stop Stop background service
317
- npx @vibe-cafe/vibe-usage daemon restart Restart background service
318
- npx @vibe-cafe/vibe-usage reset Delete all data and re-upload
319
- npx @vibe-cafe/vibe-usage reset --local Delete data for this host only and re-upload (--host is a legacy alias)
320
- npx @vibe-cafe/vibe-usage skill Install skill for AI coding tools
321
- npx @vibe-cafe/vibe-usage skill --remove Remove installed skills
322
- npx @vibe-cafe/vibe-usage status Show config and detected tools
323
- npx @vibe-cafe/vibe-usage config show Show full config as JSON
324
- npx @vibe-cafe/vibe-usage config get <key> Get a config value
325
- npx @vibe-cafe/vibe-usage config set <key> <value> Set a config value
326
- npx @vibe-cafe/vibe-usage config set codexExtraHome <path> Persist another Codex Home
327
- npx @vibe-cafe/vibe-usage config add-root <tool> <path> Add a Codex, Grok, Antigravity, or Pi data root
328
- npx @vibe-cafe/vibe-usage config remove-root <tool> <path> Remove an added data root
329
- npx @vibe-cafe/vibe-usage config roots Show added data roots as JSON
330
- npx @vibe-cafe/vibe-usage help Show this help
331
- `);
376
+ console.log(args.includes('--all') ? FULL_HELP : SHORT_HELP);
332
377
  break;
333
378
  }
334
379
  case undefined: {
@@ -338,7 +383,7 @@ export async function run(rawArgs) {
338
383
  if (!config?.apiKey || apiKey) {
339
384
  // First run OR user passed --key for a one-shot setup — init.js prints the big header
340
385
  const { runInit } = await import('./init.js');
341
- await runInit({ apiKey, codexExtraHome });
386
+ await runInit({ apiKey, codexExtraHome, noDaemon });
342
387
  } else {
343
388
  // Already configured: small header + sync
344
389
  printSmallHeader();
package/src/init.js CHANGED
@@ -6,6 +6,7 @@ import { fetchAccount, ingest, requestDeviceCode, pollDeviceCode } from './api.j
6
6
  import { runSync } from './sync.js';
7
7
  import { detectInstalledTools } from './tools.js';
8
8
  import { bigHeader, success, failure, warn, arrow, link, dim, divider } from './output.js';
9
+ import { manageDaemon, isDaemonInstalled, isDaemonPlatform } from './daemon-service.js';
9
10
 
10
11
  const CLIENT_NAME = 'vibe-usage CLI';
11
12
 
@@ -26,12 +27,8 @@ function openBrowser(url) {
26
27
  execFile(cmd, [url], () => {});
27
28
  }
28
29
 
29
- function isDaemonPlatform() {
30
- return process.platform === 'linux' || process.platform === 'darwin' || process.platform === 'win32';
31
- }
32
-
33
30
  export async function runInit(options = {}) {
34
- const { apiKey: providedKey, codexExtraHome } = options;
31
+ const { apiKey: providedKey, codexExtraHome, noDaemon = false } = options;
35
32
 
36
33
  console.log(bigHeader());
37
34
 
@@ -108,22 +105,21 @@ export async function runInit(options = {}) {
108
105
 
109
106
  await runSync({ codexExtraHome });
110
107
 
111
- if (isDaemonPlatform()) {
112
- if (process.stdin.isTTY) {
113
- console.log();
114
- const answer = await prompt(`开启后台自动同步?${dim('(推荐)')} [Y/n] `);
115
- const normalized = answer.toLowerCase();
116
- if (normalized === '' || normalized === 'y' || normalized === 'yes') {
117
- const { manageDaemon } = await import('./daemon-service.js');
118
- await manageDaemon('install');
119
- } else {
120
- console.log();
121
- console.log(dim('随时运行 `npx @vibe-cafe/vibe-usage daemon install` 开启后台同步。'));
122
- }
123
- } else {
124
- console.log();
125
- console.log(dim('提示: 运行 `npx @vibe-cafe/vibe-usage daemon install` 开启后台自动同步。'));
126
- }
108
+ // Background sync is the default (maintainer decision 2026-09-09): one
109
+ // command should leave the machine fully set up. `--no-daemon` opts out;
110
+ // a non-interactive run (CI, headless --manual-key) never installs a
111
+ // service on a machine nobody is looking at.
112
+ console.log();
113
+ if (noDaemon) {
114
+ console.log(dim('已按 --no-daemon 跳过后台同步。随时运行 `npx @vibe-cafe/vibe-usage daemon install` 开启。'));
115
+ } else if (!isDaemonPlatform()) {
116
+ console.log(dim('当前平台不支持后台同步,之后手动运行 `npx @vibe-cafe/vibe-usage` 即可同步。'));
117
+ } else if (isDaemonInstalled()) {
118
+ console.log(success('后台自动同步已在运行。'));
119
+ } else if (process.stdin.isTTY) {
120
+ await manageDaemon('install');
121
+ } else {
122
+ console.log(dim('非交互环境未开启后台同步;运行 `npx @vibe-cafe/vibe-usage daemon install` 开启。'));
127
123
  }
128
124
  }
129
125
 
package/src/output.js CHANGED
@@ -41,6 +41,17 @@ export const arrow = (msg) => `${cyan('→')} ${msg}`;
41
41
 
42
42
  export const divider = () => dim('─'.repeat(48));
43
43
 
44
+ /**
45
+ * One dim advisory line pointing at the simpler way to do what the user just
46
+ * did. Printed only when a human is watching: the Mac and Windows apps drive
47
+ * `sync` through a pipe and read stdout as the result / error text, so an
48
+ * extra line must never reach them. VIBE_USAGE_FORCE_HINTS=1 is for tests.
49
+ */
50
+ export function hint(msg) {
51
+ if (!process.stdout.isTTY && process.env.VIBE_USAGE_FORCE_HINTS !== '1') return;
52
+ console.log(dim(`提示: ${msg}`));
53
+ }
54
+
44
55
  /** Print a blank line. */
45
56
  export const nl = () => console.log();
46
57
 
package/src/reset.js CHANGED
@@ -30,7 +30,7 @@ export async function runReset(args = [], deps = {}) {
30
30
  const hostOnly = args.includes('--local') || args.includes('--host');
31
31
  const config = loadConfig();
32
32
  if (!config?.apiKey) {
33
- console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage init`。'));
33
+ console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage`。'));
34
34
  process.exit(1);
35
35
  }
36
36
 
package/src/summary.js CHANGED
@@ -6,7 +6,7 @@ export async function runSummary(args = []) {
6
6
  const days = parseDays(args);
7
7
  const config = loadConfig();
8
8
  if (!config?.apiKey) {
9
- console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage init`。'));
9
+ console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage`。'));
10
10
  process.exit(1);
11
11
  }
12
12
 
@@ -42,7 +42,7 @@ function render(data, days, apiUrl) {
42
42
  const dashboard = `${apiUrl}/usage`;
43
43
 
44
44
  if (buckets.length === 0) {
45
- return `# Vibe Usage Summary (Last ${days} ${days === 1 ? 'day' : 'days'})\n\n暂无数据。运行 \`npx @vibe-cafe/vibe-usage sync\` 上传本地 token 记录。\n\n详情: ${dashboard}\n`;
45
+ return `# Vibe Usage Summary (Last ${days} ${days === 1 ? 'day' : 'days'})\n\n暂无数据。运行 \`npx @vibe-cafe/vibe-usage\` 上传本地 token 记录。\n\n详情: ${dashboard}\n`;
46
46
  }
47
47
 
48
48
  let totalCost = 0;
package/src/sync.js CHANGED
@@ -86,7 +86,7 @@ export async function runSync({
86
86
  } = {}) {
87
87
  const config = loadConfig();
88
88
  if (!config?.apiKey) {
89
- console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage init`。'));
89
+ console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage`。'));
90
90
  if (throws) throw new Error('NOT_CONFIGURED');
91
91
  process.exit(1);
92
92
  }