@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.14.3",
3
+ "version": "0.14.4",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "scripts": {
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
- Object.entries(parsers),
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) {