@vibe-cafe/vibe-usage 0.9.16 → 0.10.1

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/src/sync.js CHANGED
@@ -34,6 +34,7 @@ export async function runSync({ throws = false, quiet = false } = {}) {
34
34
  const allBuckets = [];
35
35
  const allSessions = [];
36
36
  const parserResults = [];
37
+ const parserProgress = [];
37
38
  // Sources whose parser ran to completion this sync. pruneState() below is
38
39
  // scoped to these so a transient parser failure doesn't evict that tool's
39
40
  // state and force a full re-upload next run.
@@ -47,6 +48,14 @@ export async function runSync({ throws = false, quiet = false } = {}) {
47
48
  if (!Array.isArray(buckets) || !Array.isArray(sessions)) {
48
49
  throw new TypeError('Parser returned an invalid result');
49
50
  }
51
+ if (result?.indexing) {
52
+ parserProgress.push({ source, ...result.indexing });
53
+ }
54
+ if (Array.isArray(result?.warnings)) {
55
+ for (const message of result.warnings) {
56
+ process.stderr.write(`${dim(` ${message}`)}\n`);
57
+ }
58
+ }
50
59
  // A parser may deliberately suppress a transient error (Cursor network
51
60
  // timeout) to keep daemon logs quiet. Its empty result is not proof that
52
61
  // its prior data disappeared, so it must not be pruned this run.
@@ -71,7 +80,13 @@ export async function runSync({ throws = false, quiet = false } = {}) {
71
80
  pruneState(state, new Set(), new Set(), okSources);
72
81
  const pruned = before - (Object.keys(state.buckets).length + Object.keys(state.sessions).length);
73
82
  if (pruned > 0) saveState(state);
74
- if (!quiet) console.log(dim('暂无新数据。'));
83
+ if (!quiet && parserProgress.length > 0) {
84
+ for (const p of parserProgress) {
85
+ console.log(dim(` ${p.source}: 正在建立本地索引 ${p.completed}/${p.total}(下次同步继续)`));
86
+ }
87
+ } else if (!quiet) {
88
+ console.log(dim('暂无新数据。'));
89
+ }
75
90
  return 0;
76
91
  }
77
92
 
@@ -83,6 +98,11 @@ export async function runSync({ throws = false, quiet = false } = {}) {
83
98
  console.log(` ${dim(p.source.padEnd(14))}${parts.join(' · ')}`);
84
99
  }
85
100
  }
101
+ if (!quiet && parserProgress.length > 0) {
102
+ for (const p of parserProgress) {
103
+ console.log(dim(` ${p.source}: 正在建立本地索引 ${p.completed}/${p.total}(下次同步继续)`));
104
+ }
105
+ }
86
106
 
87
107
  let host = config.hostname;
88
108
  if (!host) {
@@ -112,10 +132,11 @@ export async function runSync({ throws = false, quiet = false } = {}) {
112
132
  for (const s of allSessions) s.project = 'unknown';
113
133
  }
114
134
 
115
- // Incremental diff: parsers above always read the full local history (cheap,
116
- // local-only). Here we drop anything whose content matches what we already
117
- // uploaded, so only new/changed items go over the network. A quiet machine
118
- // sends zero bytes; an active one sends just the current 30-min bucket.
135
+ // Incremental upload diff: parsers above emit a complete view of live local
136
+ // data (Codex may assemble that view from its disposable parser cache). Here
137
+ // we drop anything whose content matches what we already uploaded, so only
138
+ // new/changed items go over the network. A quiet machine sends zero bytes;
139
+ // an active one sends just the current 30-min bucket.
119
140
  // Missing/corrupt state.json => empty maps => one-time full upload, then
120
141
  // incremental forever after.
121
142
  const state = loadState();
@@ -169,6 +190,8 @@ export async function runSync({ throws = false, quiet = false } = {}) {
169
190
  let totalIngested = 0;
170
191
  let totalSessionsSynced = 0;
171
192
  let totalDroppedBuckets = 0;
193
+ let totalDroppedUnknownModels = 0;
194
+ let totalDroppedImplausible = 0;
172
195
  const droppedSources = new Set();
173
196
  const bucketBatches = Math.ceil(allBucketsToSend.length / BATCH_SIZE);
174
197
  const sessionBatches = Math.ceil(allSessionsToSend.length / SESSION_BATCH_SIZE);
@@ -189,8 +212,11 @@ export async function runSync({ throws = false, quiet = false } = {}) {
189
212
  }, batchSessions.length > 0 ? batchSessions : undefined);
190
213
  totalIngested += result.ingested ?? batch.length;
191
214
  totalSessionsSynced += result.sessions ?? 0;
215
+ const batchUnknownSources = new Set(result.dropped?.unknownSources || []);
192
216
  if (result.dropped) {
193
217
  totalDroppedBuckets += Number(result.dropped.buckets) || 0;
218
+ totalDroppedUnknownModels += Number(result.dropped.unknownModels) || 0;
219
+ totalDroppedImplausible += Number(result.dropped.implausible) || 0;
194
220
  for (const s of result.dropped.unknownSources || []) droppedSources.add(s);
195
221
  }
196
222
 
@@ -199,6 +225,10 @@ export async function runSync({ throws = false, quiet = false } = {}) {
199
225
  // state, so the next sync re-sends exactly those items — no data loss,
200
226
  // no silent gaps.
201
227
  for (const b of batch) {
228
+ // A source unknown to an older backend may become valid after deploy.
229
+ // Leave those hashes uncommitted so the next sync retries them instead
230
+ // of turning a temporary release-order mismatch into permanent loss.
231
+ if (batchUnknownSources.has(b.source)) continue;
202
232
  const key = bucketKey(b);
203
233
  const entry = pendingBucketState.get(key);
204
234
  if (entry) state.buckets[key] = entry;
@@ -219,11 +249,14 @@ export async function runSync({ throws = false, quiet = false } = {}) {
219
249
  console.log(success(`已同步 ${syncParts.join(' · ')}`));
220
250
 
221
251
  if (totalDroppedBuckets > 0) {
222
- // Server doesn't (yet) recognize these source IDs — usually means the
223
- // CLI is newer than the deployed vibe-cafe. Surface so the user knows
224
- // the data wasn't lost on their end, just not stored upstream.
225
- const sourcesList = Array.from(droppedSources).sort().join(', ');
226
- console.log(dim(` ${totalDroppedBuckets} buckets dropped (服务端未收录的 source: ${sourcesList})`));
252
+ const reasons = [];
253
+ if (droppedSources.size > 0) {
254
+ reasons.push(`服务端未收录的 source: ${Array.from(droppedSources).sort().join(', ')}`);
255
+ }
256
+ if (totalDroppedUnknownModels > 0) reasons.push(`模型未知: ${totalDroppedUnknownModels}`);
257
+ if (totalDroppedImplausible > 0) reasons.push(`超出合理范围: ${totalDroppedImplausible}`);
258
+ if (reasons.length === 0) reasons.push('服务端拒绝');
259
+ console.log(dim(` ${totalDroppedBuckets} buckets dropped (${reasons.join(';')})`));
227
260
  }
228
261
 
229
262
  if (!quiet && totalSessionsSynced > 0) {
package/src/tools.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
+ import { findClaudeCodeDataDirs } from './claude-roots.js';
4
5
 
5
6
  function getCursorStateDbPath() {
6
7
  const rel = join('User', 'globalStorage', 'state.vscdb');
@@ -80,20 +81,6 @@ function findOpenclawDataDirs() {
80
81
  return dirs;
81
82
  }
82
83
 
83
- // Claude Code lives in ~/.claude/projects, but $CLAUDE_CONFIG_DIR relocates its
84
- // whole tree. Detect either so a user who only set CLAUDE_CONFIG_DIR is still
85
- // recognized (the parser scans both roots; see parsers/claude-code.js).
86
- function findClaudeCodeDataDirs() {
87
- const dirs = [join(homedir(), '.claude', 'projects')];
88
- const cfg = process.env.CLAUDE_CONFIG_DIR?.trim();
89
- if (cfg) {
90
- let custom = cfg.startsWith('~') ? join(homedir(), cfg.slice(1)) : cfg;
91
- custom = custom.replace(/[/\\]+$/, '') || custom;
92
- dirs.push(join(custom, 'projects'));
93
- }
94
- return dirs.filter(existsSync);
95
- }
96
-
97
84
  // Codex keeps live sessions in ~/.codex/sessions and moves completed ones to
98
85
  // ~/.codex/archived_sessions. Detect Codex if either dir exists, so a user
99
86
  // whose sessions have all been archived is still recognized.