@vibe-cafe/vibe-usage 0.10.24 → 0.10.26
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 +30 -27
- package/package.json +1 -1
- package/src/daemon-service.js +138 -35
- package/src/daemon.js +1 -1
- package/src/index.js +80 -35
- package/src/init.js +17 -21
- package/src/output.js +11 -0
- package/src/parsers/cursor.js +9 -3
- package/src/reset.js +1 -1
- package/src/summary.js +2 -2
- package/src/sync.js +9 -11
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
|
|
@@ -102,7 +111,7 @@ Cursor 用量需要从 `cursor.com` 下载 CSV。`Cursor usage export skipped (n
|
|
|
102
111
|
- `ENOTFOUND` / `EAI_AGAIN`:检查 DNS 和终端网络。
|
|
103
112
|
- `UND_ERR_CONNECT_TIMEOUT` / `ETIMEDOUT` / `ECONNRESET`:检查到 Cursor 的连接及终端代理。浏览器或 Cursor 应用能联网,不代表 Node.js 使用了相同代理。
|
|
104
113
|
- `CERT_*` / `UNABLE_TO_VERIFY_LEAF_SIGNATURE` 等:检查系统时间和代理或公司网络的 CA 证书配置;自定义 CA 可通过 `NODE_EXTRA_CA_CERTS` 指定。
|
|
105
|
-
- `timeout after …ms
|
|
114
|
+
- `timeout after …ms`:导出超时。`cursor.com` 的导出是按整个账号现算的,**账号用量越大越慢**,所以重度用户会每次都超时、而不是偶尔。默认等待 120 秒;仍不够就调大,例如 `VIBE_USAGE_CURSOR_FETCH_TIMEOUT_MS=300000`,然后重跑一次 `sync` 补传历史。
|
|
106
115
|
- `Cursor session rejected`:在 Cursor 的 Account 设置中重新登录,再同步。
|
|
107
116
|
|
|
108
117
|
需要 HTTP/HTTPS 代理时,Node.js **22.21+ 或 24.5+** 可用 `NODE_USE_ENV_PROXY=1` 启用环境变量代理([Node.js 官方说明](https://nodejs.org/en/learn/http/enterprise-network-configuration))。以下为 macOS/Linux 终端示例,代理地址须替换为实际地址:
|
|
@@ -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
|
-
##
|
|
215
|
-
|
|
216
|
-
### Background service (recommended)
|
|
223
|
+
## Background sync
|
|
217
224
|
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
246
|
+
</details>
|
|
244
247
|
|
|
245
248
|
## License
|
|
246
249
|
|
package/package.json
CHANGED
package/src/daemon-service.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
154
|
+
ExecStart=${serviceArgv(nodePath, binPath, launcher).join(' ')}
|
|
117
155
|
Restart=on-failure
|
|
118
|
-
RestartSec
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
`"${escapeCmdValue(
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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('
|
|
425
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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/parsers/cursor.js
CHANGED
|
@@ -68,9 +68,15 @@ function decodeJwtSub(token) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
71
|
+
// cursor.com's CSV export is computed over the whole account, so it gets
|
|
72
|
+
// slower the more usage the account has: heavy users time out on every run,
|
|
73
|
+
// not intermittently. 10s (v0.7.11) and then 30s (#72) were both still short
|
|
74
|
+
// enough to lock those accounts out permanently -- a user with ~13B tokens/30d
|
|
75
|
+
// needed 120s to complete a single export, and every default-timeout run of
|
|
76
|
+
// his silently uploaded nothing. Budget for the slow accounts; a healthy
|
|
77
|
+
// export returns in a second or two, so this ceiling only costs wall-clock
|
|
78
|
+
// time on the runs that were failing anyway.
|
|
79
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 120_000;
|
|
74
80
|
const MAX_FETCH_TIMEOUT_MS = 2_147_483_647;
|
|
75
81
|
|
|
76
82
|
export function resolveCursorFetchTimeout(value) {
|
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
|
|
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
|
|
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
|
|
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
|
@@ -21,12 +21,6 @@ function formatBytes(bytes) {
|
|
|
21
21
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/** Hide only Cursor's intentional transient fetch soft-skip in quiet (daemon) syncs. */
|
|
25
|
-
export function shouldSuppressParserWarning(source, message, quiet) {
|
|
26
|
-
if (!quiet) return false;
|
|
27
|
-
return source === 'cursor' && message.startsWith('cursor: Cursor usage export skipped (');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
24
|
export function resolveUploadProjectSetting(settings) {
|
|
31
25
|
if (typeof settings?.uploadProject !== 'boolean') {
|
|
32
26
|
const error = new Error('SETTINGS_UNAVAILABLE');
|
|
@@ -86,7 +80,7 @@ export async function runSync({
|
|
|
86
80
|
} = {}) {
|
|
87
81
|
const config = loadConfig();
|
|
88
82
|
if (!config?.apiKey) {
|
|
89
|
-
console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage
|
|
83
|
+
console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage`。'));
|
|
90
84
|
if (throws) throw new Error('NOT_CONFIGURED');
|
|
91
85
|
process.exit(1);
|
|
92
86
|
}
|
|
@@ -184,13 +178,17 @@ export async function runSync({
|
|
|
184
178
|
if (indexing) {
|
|
185
179
|
parserProgress.push({ source, ...indexing });
|
|
186
180
|
}
|
|
181
|
+
// Parser warnings always reach stderr, including quiet (daemon) runs: the
|
|
182
|
+
// daemon log is the only trail a background failure leaves. Cursor's fetch
|
|
183
|
+
// soft-skip used to be filtered out here to keep that log tidy, which made
|
|
184
|
+
// a permanently failing export indistinguishable from a healthy one -- the
|
|
185
|
+
// tool still listed as "installed" while it had never uploaded a byte.
|
|
187
186
|
for (const message of warnings) {
|
|
188
|
-
if (shouldSuppressParserWarning(source, message, quiet)) continue;
|
|
189
187
|
process.stderr.write(`${dim(` ${message}`)}\n`);
|
|
190
188
|
}
|
|
191
|
-
// A parser may
|
|
192
|
-
//
|
|
193
|
-
//
|
|
189
|
+
// A parser may downgrade a transient error (Cursor network timeout) to a
|
|
190
|
+
// warning instead of throwing. Its empty result is not proof that its prior
|
|
191
|
+
// data disappeared, so it must not be pruned this run.
|
|
194
192
|
if (!skipped) okSources.add(source);
|
|
195
193
|
for (const bucket of buckets) allBuckets.push(bucket);
|
|
196
194
|
for (const session of sessions) allSessions.push(session);
|