@vibe-cafe/vibe-usage 0.14.3 → 0.14.4
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 +1 -0
- package/package.json +1 -1
- package/src/output.js +59 -0
- package/src/sync.js +21 -2
package/README.md
CHANGED
|
@@ -144,6 +144,7 @@ To relabel old history, the maintainer needs the old bucket keys, raw per-tool t
|
|
|
144
144
|
- Uploads buckets + sessions to your vibecafe.ai dashboard (always gzip-compressed, ~94% smaller)
|
|
145
145
|
- Incremental upload: every parser emits a complete local snapshot, then only buckets/sessions that are new or changed since the last successful upload are sent — a quiet machine uploads nothing. Upload state remains in `~/.vibe-usage/state.json`; failed or still-indexing parsers retain their prior state, while deleted local logs are pruned. Deleting the state file triggers a one-time full re-upload, and `reset` clears it automatically after deleting cloud data
|
|
146
146
|
- 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. Multi-file sessions have a combined cache that rebuilds when any segment changes. A bounded rolling audit occasionally re-reads one historical file or continuation group. Very large first-time indexes checkpoint before the Mac app timeout and resume on the next sync instead of restarting
|
|
147
|
+
- Interactive `sync` shows a live status line (`正在读取上传设置` → `正在扫描本地日志 12/35 · 已用时 2m10s`) while it reads backend settings and scans/indexes local logs. A first sync, a cold Codex index rebuild, or a slow `cursor.com` export can take minutes, so that line is what separates a slow sync from a hung one. It is printed only on a TTY — the Mac and Windows apps read the piped stdout as the result text and must keep receiving plain output. Interrupting a sync is safe: finished batches are already committed, and the Codex index checkpoints and resumes on the next run
|
|
147
148
|
- 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
|
|
148
149
|
- 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
|
|
149
150
|
- 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
|
package/package.json
CHANGED
package/src/output.js
CHANGED
|
@@ -52,6 +52,65 @@ export function hint(msg) {
|
|
|
52
52
|
console.log(dim(`提示: ${msg}`));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* One redrawn status line for a phase that would otherwise leave the terminal
|
|
57
|
+
* blank for minutes: the settings round-trip (a timing-out endpoint re-trying),
|
|
58
|
+
* the local log scan, and a cold Codex index build. Without it the sync header
|
|
59
|
+
* is the last thing on screen, and "still working" is indistinguishable from
|
|
60
|
+
* "hung" — exactly how a slow first sync reads.
|
|
61
|
+
*
|
|
62
|
+
* TTY-only for the same reason as hint(): the Mac (`SyncEngine.swift`) and
|
|
63
|
+
* Windows (`sync_engine.rs`) apps run `sync` through a pipe and treat stdout as
|
|
64
|
+
* the result text, so a pipe must never receive these lines.
|
|
65
|
+
* `VIBE_USAGE_FORCE_PROGRESS=1` forces them on for tests, and
|
|
66
|
+
* `VIBE_USAGE_PROGRESS_INTERVAL_MS` overrides the redraw interval.
|
|
67
|
+
*
|
|
68
|
+
* `label` is a function so the caller owns the wording and its elapsed-time
|
|
69
|
+
* text; it is re-read on every redraw. The interval is unref'ed so a phase that
|
|
70
|
+
* never reaches `stop()` cannot hold the process open.
|
|
71
|
+
*/
|
|
72
|
+
export function liveStatus({ enabled = true, intervalMs } = {}) {
|
|
73
|
+
const configuredInterval = Number(process.env.VIBE_USAGE_PROGRESS_INTERVAL_MS);
|
|
74
|
+
const tickMs = intervalMs ?? (configuredInterval > 0 ? configuredInterval : 5000);
|
|
75
|
+
const active = enabled
|
|
76
|
+
&& (!!process.stdout.isTTY || process.env.VIBE_USAGE_FORCE_PROGRESS === '1');
|
|
77
|
+
let label = null;
|
|
78
|
+
let timer = null;
|
|
79
|
+
let startedAt = 0;
|
|
80
|
+
|
|
81
|
+
function draw() {
|
|
82
|
+
if (label === null) return;
|
|
83
|
+
process.stdout.write(`\r${dim(` ${label(Date.now() - startedAt)}`)}\x1b[K`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
/** Show the line, or retarget it to the next phase. */
|
|
88
|
+
start(nextLabel) {
|
|
89
|
+
if (!active) return;
|
|
90
|
+
label = nextLabel;
|
|
91
|
+
startedAt = Date.now();
|
|
92
|
+
draw();
|
|
93
|
+
if (timer === null) {
|
|
94
|
+
timer = setInterval(draw, tickMs);
|
|
95
|
+
unrefTimer(timer);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
/** Erase the line before regular output continues on it. */
|
|
99
|
+
stop() {
|
|
100
|
+
if (!active || label === null) return;
|
|
101
|
+
clearInterval(timer);
|
|
102
|
+
timer = null;
|
|
103
|
+
label = null;
|
|
104
|
+
process.stdout.write('\r\x1b[K');
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Bun's timer objects and Node's both expose unref; tests run on either. */
|
|
110
|
+
function unrefTimer(timer) {
|
|
111
|
+
if (typeof timer?.unref === 'function') timer.unref();
|
|
112
|
+
}
|
|
113
|
+
|
|
55
114
|
/** Print a blank line. */
|
|
56
115
|
export const nl = () => console.log();
|
|
57
116
|
|
package/src/sync.js
CHANGED
|
@@ -11,7 +11,7 @@ import { aggregateToBuckets } from './parsers/aggregate.js';
|
|
|
11
11
|
import { normalizeParserResult } from './parsers/contract.js';
|
|
12
12
|
import { extraRootList } from './extra-roots.js';
|
|
13
13
|
import { planKikiMigration } from './kiki-migration.js';
|
|
14
|
-
import { success, failure, warn, arrow, link, dim } from './output.js';
|
|
14
|
+
import { success, failure, warn, arrow, link, dim, liveStatus } from './output.js';
|
|
15
15
|
|
|
16
16
|
const BATCH_SIZE = 100;
|
|
17
17
|
const SESSION_BATCH_SIZE = 500;
|
|
@@ -44,6 +44,11 @@ function formatBytes(bytes) {
|
|
|
44
44
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** Elapsed-time suffix for the live status line (`formatDuration` rounds 0 to `0s`). */
|
|
48
|
+
function phaseElapsed(elapsedMs) {
|
|
49
|
+
return ` · 已用时 ${formatDuration(elapsedMs / 1000)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
export function resolveUploadProjectSetting(settings) {
|
|
48
53
|
if (typeof settings?.uploadProject !== 'boolean') {
|
|
49
54
|
const error = new Error('SETTINGS_UNAVAILABLE');
|
|
@@ -127,6 +132,13 @@ export async function runSync({
|
|
|
127
132
|
// Built from the same `apiUrl` the ingest calls below use, so the recorded
|
|
128
133
|
// target and the actual target can never drift apart.
|
|
129
134
|
const identity = stateIdentity({ apiUrl, apiKey: config.apiKey });
|
|
135
|
+
// Both the settings round-trip and the log scan below can run for minutes on
|
|
136
|
+
// a cold or proxied machine (a timing-out endpoint retrying, a cold Codex
|
|
137
|
+
// index, a 120s Cursor export). A live status line keeps that work
|
|
138
|
+
// distinguishable from a hang; it never reaches a pipe, so the desktop apps
|
|
139
|
+
// keep parsing bare result text from stdout.
|
|
140
|
+
const status = liveStatus({ enabled: !quiet });
|
|
141
|
+
status.start(elapsedMs => `正在读取上传设置${phaseElapsed(elapsedMs)}`);
|
|
130
142
|
let uploadProject;
|
|
131
143
|
try {
|
|
132
144
|
const settings = await fetchSettings(apiUrl, config.apiKey);
|
|
@@ -143,6 +155,7 @@ export async function runSync({
|
|
|
143
155
|
saveConfig(config);
|
|
144
156
|
}
|
|
145
157
|
} catch (err) {
|
|
158
|
+
status.stop();
|
|
146
159
|
if (err.message === 'UNAUTHORIZED') {
|
|
147
160
|
console.error(failure('API Key 无效,请运行 `npx @vibe-cafe/vibe-usage init` 重新配置。'));
|
|
148
161
|
if (throws) throw err;
|
|
@@ -173,8 +186,11 @@ export async function runSync({
|
|
|
173
186
|
// Run parsers concurrently (bounded) so one slow parser (Cursor's network
|
|
174
187
|
// fetch, a cold Codex index) can't stall the rest. Results are collected in
|
|
175
188
|
// registry order so output and merged arrays stay deterministic.
|
|
189
|
+
const parserRegistry = Object.entries(parsers);
|
|
190
|
+
let parsedSources = 0;
|
|
191
|
+
status.start(elapsedMs => `正在扫描本地日志 ${parsedSources}/${parserRegistry.length}${phaseElapsed(elapsedMs)}`);
|
|
176
192
|
const parserOutcomes = await mapWithConcurrency(
|
|
177
|
-
|
|
193
|
+
parserRegistry,
|
|
178
194
|
PARSER_CONCURRENCY,
|
|
179
195
|
async ([source, parse]) => {
|
|
180
196
|
try {
|
|
@@ -187,9 +203,12 @@ export async function runSync({
|
|
|
187
203
|
return { source, result };
|
|
188
204
|
} catch (err) {
|
|
189
205
|
return { source, error: err };
|
|
206
|
+
} finally {
|
|
207
|
+
parsedSources++;
|
|
190
208
|
}
|
|
191
209
|
},
|
|
192
210
|
);
|
|
211
|
+
status.stop();
|
|
193
212
|
|
|
194
213
|
for (const { source, result, error } of parserOutcomes) {
|
|
195
214
|
if (error) {
|