@vibe-cafe/vibe-usage 0.10.23 → 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 +29 -26
- package/package.json +1 -1
- package/src/api.js +33 -0
- package/src/daemon-service.js +138 -35
- package/src/daemon.js +1 -1
- package/src/index.js +107 -35
- package/src/init.js +25 -22
- package/src/output.js +11 -0
- package/src/reset.js +1 -1
- package/src/summary.js +2 -2
- package/src/sync.js +1 -1
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
|
|
|
@@ -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/api.js
CHANGED
|
@@ -280,6 +280,39 @@ export function getJson(apiUrl, apiKey, path, { timeoutMs = 15_000 } = {}) {
|
|
|
280
280
|
});
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Which vibecafe account this API key is bound to.
|
|
285
|
+
*
|
|
286
|
+
* Exists because a key minted against the wrong account is otherwise invisible
|
|
287
|
+
* from the client side: ingest and this endpoint both answer 200 for it, so the
|
|
288
|
+
* user sees a healthy CLI and a healthy desktop app while their uploads pile up
|
|
289
|
+
* on an account they never look at. Ask the backend whose data it is and say so.
|
|
290
|
+
*
|
|
291
|
+
* Returns null when the backend is too old to send `account`, or on any
|
|
292
|
+
* transient failure — identity is a nicety, never a reason to fail a command.
|
|
293
|
+
* UNAUTHORIZED still propagates: that one the caller must not swallow.
|
|
294
|
+
*
|
|
295
|
+
* @param {string} apiUrl
|
|
296
|
+
* @param {string} apiKey
|
|
297
|
+
* @returns {Promise<{handle: string, name: string | null} | null>}
|
|
298
|
+
*/
|
|
299
|
+
export async function fetchAccount(apiUrl, apiKey) {
|
|
300
|
+
let data;
|
|
301
|
+
try {
|
|
302
|
+
// bucketsOnly keeps the response to the aggregate we throw away; days=1
|
|
303
|
+
// keeps the window (and the scan) small.
|
|
304
|
+
data = await getJson(apiUrl, apiKey, '/api/usage?days=1&bucketsOnly=true', { timeoutMs: 10_000 });
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (err.message === 'UNAUTHORIZED') throw err;
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const account = data?.account;
|
|
310
|
+
if (account && typeof account.handle === 'string') {
|
|
311
|
+
return { handle: account.handle, name: account.name ?? null };
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
283
316
|
/**
|
|
284
317
|
* GET user settings from the vibecafe API.
|
|
285
318
|
* Returns null after transient failures are exhausted. A 401 remains distinct
|
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,8 @@ import {
|
|
|
8
8
|
normalizeExtraRoot,
|
|
9
9
|
validateExtraRoot,
|
|
10
10
|
} from './extra-roots.js';
|
|
11
|
-
import { failure, smallHeader } from './output.js';
|
|
11
|
+
import { dim as dimText, failure, hint, smallHeader, warn } from './output.js';
|
|
12
|
+
import { fetchAccount } from './api.js';
|
|
12
13
|
|
|
13
14
|
function printSmallHeader() {
|
|
14
15
|
console.log();
|
|
@@ -22,11 +23,13 @@ async function showStatus() {
|
|
|
22
23
|
|
|
23
24
|
if (!config?.apiKey) {
|
|
24
25
|
console.log(' Config: not configured');
|
|
25
|
-
console.log(` Run \`npx @vibe-cafe/vibe-usage
|
|
26
|
+
console.log(` Run \`npx @vibe-cafe/vibe-usage\` to set up.\n`);
|
|
26
27
|
} else {
|
|
27
28
|
console.log(` Config: ${getConfigPath()}`);
|
|
28
29
|
console.log(` API key: ${config.apiKey.slice(0, 8)}...`);
|
|
29
30
|
console.log(` API URL: ${config.apiUrl || 'https://vibecafe.ai'}`);
|
|
31
|
+
// 数据算在谁名下,是这里最该回答、以前偏偏答不出的一件事。
|
|
32
|
+
await printBoundAccount(config.apiUrl || 'https://vibecafe.ai', config.apiKey);
|
|
30
33
|
if (config.codexExtraHome) {
|
|
31
34
|
console.log(` Extra Codex Home: ${config.codexExtraHome}`);
|
|
32
35
|
}
|
|
@@ -62,6 +65,30 @@ async function showStatus() {
|
|
|
62
65
|
console.log();
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Print the account this key uploads to. Never throws: an offline machine or an
|
|
70
|
+
* older backend just means we cannot name the account, which must not make
|
|
71
|
+
* `status` fail — but a revoked/invalid key is worth saying out loud.
|
|
72
|
+
*/
|
|
73
|
+
async function printBoundAccount(apiUrl, apiKey) {
|
|
74
|
+
try {
|
|
75
|
+
const account = await fetchAccount(apiUrl, apiKey);
|
|
76
|
+
if (account) {
|
|
77
|
+
const label = account.name ? `@${account.handle}(${account.name})` : `@${account.handle}`;
|
|
78
|
+
console.log(` 账号: ${label}`);
|
|
79
|
+
console.log(dimText(` 数据都记在这个账号名下,不是它就换个账号重新 init`));
|
|
80
|
+
} else {
|
|
81
|
+
console.log(dimText(' 账号: 服务端未返回(后端版本较旧或网络异常)'));
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err.message === 'UNAUTHORIZED') {
|
|
85
|
+
console.log(warn('账号: Key 已失效,请重新运行 `npx @vibe-cafe/vibe-usage init`'));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
console.log(dimText(' 账号: 读取失败'));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
65
92
|
const VALID_CONFIG_KEYS = ['apiKey', 'apiUrl', 'hostname', 'codexExtraHome'];
|
|
66
93
|
|
|
67
94
|
function handleConfig(args) {
|
|
@@ -186,6 +213,69 @@ function extractOption(args, name) {
|
|
|
186
213
|
return { args: [...args.slice(0, idx), ...args.slice(idx + 2)], value };
|
|
187
214
|
}
|
|
188
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
|
+
|
|
189
279
|
export async function run(rawArgs) {
|
|
190
280
|
// --key and --manual-key both mean "skip device flow, take this vbu_ key".
|
|
191
281
|
// --manual-key is the documented name; --key is kept as a legacy alias so
|
|
@@ -195,7 +285,10 @@ export async function run(rawArgs) {
|
|
|
195
285
|
({ args: stripped, value: apiKey } = extractOption(rawArgs, 'manual-key'));
|
|
196
286
|
if (apiKey === undefined) {
|
|
197
287
|
({ args: stripped, value: apiKey } = extractOption(stripped, 'key'));
|
|
288
|
+
if (apiKey !== undefined) hint('--key 已改名 --manual-key,旧写法仍可用');
|
|
198
289
|
}
|
|
290
|
+
let noDaemon;
|
|
291
|
+
({ args: stripped, value: noDaemon } = extractFlag(stripped, 'no-daemon'));
|
|
199
292
|
let codexExtraHome;
|
|
200
293
|
({ args: stripped, value: codexExtraHome } = extractOption(stripped, 'extra-codex-home'));
|
|
201
294
|
if (codexExtraHome !== undefined) {
|
|
@@ -212,14 +305,19 @@ export async function run(rawArgs) {
|
|
|
212
305
|
|
|
213
306
|
switch (command) {
|
|
214
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;
|
|
215
311
|
const { runInit } = await import('./init.js');
|
|
216
|
-
await runInit({ apiKey, codexExtraHome });
|
|
312
|
+
await runInit({ apiKey, codexExtraHome, noDaemon });
|
|
313
|
+
if (firstSetup) hint(`以后直接运行 ${BARE} 即可:首次登录,之后同步`);
|
|
217
314
|
break;
|
|
218
315
|
}
|
|
219
316
|
case 'sync': {
|
|
220
317
|
printSmallHeader();
|
|
221
318
|
const { runSync } = await import('./sync.js');
|
|
222
319
|
await runSync({ codexExtraHome });
|
|
320
|
+
hint(`以后直接运行 ${BARE} 就是同步,不用再加 sync`);
|
|
223
321
|
break;
|
|
224
322
|
}
|
|
225
323
|
case 'summary': {
|
|
@@ -229,15 +327,18 @@ export async function run(rawArgs) {
|
|
|
229
327
|
}
|
|
230
328
|
case 'reset': {
|
|
231
329
|
printSmallHeader();
|
|
330
|
+
if (args.includes('--host')) hint('reset --host 已改名 reset --local,旧写法仍可用');
|
|
232
331
|
const { runReset } = await import('./reset.js');
|
|
233
332
|
await runReset(args.slice(1));
|
|
234
333
|
break;
|
|
235
334
|
}
|
|
236
335
|
case 'daemon':
|
|
237
336
|
case '--daemon': {
|
|
337
|
+
if (command === '--daemon') hint('--daemon 已改名 daemon,旧写法仍可用');
|
|
238
338
|
const sub = args[1];
|
|
239
339
|
if (sub === undefined) {
|
|
240
340
|
// Foreground daemon loop — no header, just start syncing
|
|
341
|
+
hint(`首次运行 ${BARE} 会自动开启后台同步,不用手动跑 daemon`);
|
|
241
342
|
const { runDaemon } = await import('./daemon.js');
|
|
242
343
|
await runDaemon({ codexExtraHome });
|
|
243
344
|
} else {
|
|
@@ -251,6 +352,7 @@ export async function run(rawArgs) {
|
|
|
251
352
|
printSmallHeader();
|
|
252
353
|
const { manageDaemon } = await import('./daemon-service.js');
|
|
253
354
|
await manageDaemon(sub);
|
|
355
|
+
if (sub === 'install') hint(`首次运行 ${BARE} 会自动开启后台同步,不用单独装`);
|
|
254
356
|
}
|
|
255
357
|
break;
|
|
256
358
|
}
|
|
@@ -271,37 +373,7 @@ export async function run(rawArgs) {
|
|
|
271
373
|
case 'help':
|
|
272
374
|
case '--help':
|
|
273
375
|
case '-h': {
|
|
274
|
-
console.log(
|
|
275
|
-
vibe-usage - Vibe Usage Tracker by VibeCafé
|
|
276
|
-
|
|
277
|
-
Usage:
|
|
278
|
-
npx @vibe-cafe/vibe-usage Init (first run, browser login) or sync
|
|
279
|
-
npx @vibe-cafe/vibe-usage init Set up via browser login (default)
|
|
280
|
-
npx @vibe-cafe/vibe-usage init --manual-key <vbu_...> Skip browser, use a pre-issued key (CI/headless)
|
|
281
|
-
npx @vibe-cafe/vibe-usage sync Manually sync usage data
|
|
282
|
-
npx @vibe-cafe/vibe-usage sync --extra-codex-home <path> Use another Codex Home for this run
|
|
283
|
-
npx @vibe-cafe/vibe-usage summary Print last 7 days as markdown (cost/tokens/model/project)
|
|
284
|
-
npx @vibe-cafe/vibe-usage summary --days N Same, but over the last N days (1-90)
|
|
285
|
-
npx @vibe-cafe/vibe-usage daemon Continuous sync (every 30m, foreground)
|
|
286
|
-
npx @vibe-cafe/vibe-usage daemon install Install background service (systemd/launchd/Task Scheduler)
|
|
287
|
-
npx @vibe-cafe/vibe-usage daemon uninstall Remove background service
|
|
288
|
-
npx @vibe-cafe/vibe-usage daemon status Show background service status
|
|
289
|
-
npx @vibe-cafe/vibe-usage daemon stop Stop background service
|
|
290
|
-
npx @vibe-cafe/vibe-usage daemon restart Restart background service
|
|
291
|
-
npx @vibe-cafe/vibe-usage reset Delete all data and re-upload
|
|
292
|
-
npx @vibe-cafe/vibe-usage reset --local Delete data for this host only and re-upload (--host is a legacy alias)
|
|
293
|
-
npx @vibe-cafe/vibe-usage skill Install skill for AI coding tools
|
|
294
|
-
npx @vibe-cafe/vibe-usage skill --remove Remove installed skills
|
|
295
|
-
npx @vibe-cafe/vibe-usage status Show config and detected tools
|
|
296
|
-
npx @vibe-cafe/vibe-usage config show Show full config as JSON
|
|
297
|
-
npx @vibe-cafe/vibe-usage config get <key> Get a config value
|
|
298
|
-
npx @vibe-cafe/vibe-usage config set <key> <value> Set a config value
|
|
299
|
-
npx @vibe-cafe/vibe-usage config set codexExtraHome <path> Persist another Codex Home
|
|
300
|
-
npx @vibe-cafe/vibe-usage config add-root <tool> <path> Add a Codex, Grok, Antigravity, or Pi data root
|
|
301
|
-
npx @vibe-cafe/vibe-usage config remove-root <tool> <path> Remove an added data root
|
|
302
|
-
npx @vibe-cafe/vibe-usage config roots Show added data roots as JSON
|
|
303
|
-
npx @vibe-cafe/vibe-usage help Show this help
|
|
304
|
-
`);
|
|
376
|
+
console.log(args.includes('--all') ? FULL_HELP : SHORT_HELP);
|
|
305
377
|
break;
|
|
306
378
|
}
|
|
307
379
|
case undefined: {
|
|
@@ -311,7 +383,7 @@ export async function run(rawArgs) {
|
|
|
311
383
|
if (!config?.apiKey || apiKey) {
|
|
312
384
|
// First run OR user passed --key for a one-shot setup — init.js prints the big header
|
|
313
385
|
const { runInit } = await import('./init.js');
|
|
314
|
-
await runInit({ apiKey, codexExtraHome });
|
|
386
|
+
await runInit({ apiKey, codexExtraHome, noDaemon });
|
|
315
387
|
} else {
|
|
316
388
|
// Already configured: small header + sync
|
|
317
389
|
printSmallHeader();
|
package/src/init.js
CHANGED
|
@@ -2,10 +2,11 @@ import { createInterface } from 'node:readline';
|
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { hostname as osHostname, platform } from 'node:os';
|
|
4
4
|
import { loadConfig, saveConfig } from './config.js';
|
|
5
|
-
import { ingest, requestDeviceCode, pollDeviceCode } from './api.js';
|
|
5
|
+
import { fetchAccount, ingest, requestDeviceCode, pollDeviceCode } from './api.js';
|
|
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
|
|
|
@@ -68,6 +65,13 @@ export async function runInit(options = {}) {
|
|
|
68
65
|
try {
|
|
69
66
|
await ingest(apiUrl, apiKey, []);
|
|
70
67
|
console.log(success(`验证通过 ${dim(apiKey.slice(0, 12) + '...')}`));
|
|
68
|
+
// 说清楚数据会算在谁名下 —— 授权那一刻浏览器里登录的可能并不是他自己以为的
|
|
69
|
+
// 那个账号,而这是整条链路上最后一次能当场发现的机会。
|
|
70
|
+
const account = await fetchAccount(apiUrl, apiKey).catch(() => null);
|
|
71
|
+
if (account) {
|
|
72
|
+
console.log(success(`已链接到账号 ${account.name ? `@${account.handle}(${account.name})` : `@${account.handle}`}`));
|
|
73
|
+
console.log(dim(' 数据都会记在这个账号名下;不是你要的账号就退出登录后重新 init。'));
|
|
74
|
+
}
|
|
71
75
|
} catch (err) {
|
|
72
76
|
if (err.message === 'UNAUTHORIZED') {
|
|
73
77
|
console.error(failure('API Key 无效,请检查后重试。'));
|
|
@@ -101,22 +105,21 @@ export async function runInit(options = {}) {
|
|
|
101
105
|
|
|
102
106
|
await runSync({ codexExtraHome });
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
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` 开启。'));
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
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
|
|
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
|
@@ -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
|
|
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
|
}
|