ccsniff 1.1.18 → 1.1.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccsniff",
3
- "version": "1.1.18",
3
+ "version": "1.1.19",
4
4
  "description": "Watch Claude Code JSONL output files and emit structured events as a Node.js EventEmitter",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -195,7 +195,7 @@ export function watch(projectsDir) {
195
195
  export class JsonlReplayer extends JsonlWatcher {
196
196
  constructor(projectsDir = DEFAULT_DIR) { super(projectsDir); }
197
197
 
198
- replay({ since = 0, files: fileFilter = null } = {}) {
198
+ replay({ since = 0, files: fileFilter = null, maxEvents = 0 } = {}) {
199
199
  const all = [];
200
200
  const collect = (dir, depth) => {
201
201
  if (depth > 5) return;
@@ -208,9 +208,19 @@ export class JsonlReplayer extends JsonlWatcher {
208
208
  } catch {}
209
209
  };
210
210
  if (fs.existsSync(this._dir)) collect(this._dir, 0);
211
- const chosen = fileFilter ? all.filter(fileFilter) : all;
211
+ let chosen = fileFilter ? all.filter(fileFilter) : all;
212
+ // When a maxEvents budget is set, read newest files first and stop once the
213
+ // budget is met — so a huge projects tree never has its full backlog parsed
214
+ // into the heap at once (the load-time memory peak this guards against).
215
+ if (maxEvents > 0) {
216
+ chosen = chosen
217
+ .map(fp => { try { return { fp, m: fs.statSync(fp).mtimeMs }; } catch { return { fp, m: 0 }; } })
218
+ .sort((a, b) => b.m - a.m)
219
+ .map(x => x.fp);
220
+ }
212
221
  let emitted = 0;
213
222
  for (const fp of chosen) {
223
+ if (maxEvents > 0 && emitted >= maxEvents) break;
214
224
  const fallbackSid = path.basename(fp, '.jsonl');
215
225
  let data;
216
226
  try { data = fs.readFileSync(fp, 'utf8'); } catch { continue; }
package/src/store.js CHANGED
@@ -97,8 +97,16 @@ export class Store {
97
97
  if (this.events.length > softCap) this.events.splice(0, this.events.length - this.maxEvents);
98
98
  });
99
99
  r.on('streaming_error', ev => { this.errors.push({ ts: ev.timestamp, sid: ev.conversationId, error: ev.error, recoverable: ev.recoverable }); });
100
- const stats = r.replay({});
100
+ // Bound the replay to a little above the retention cap and read newest-first,
101
+ // so a large projects tree never parses its entire backlog into the heap at
102
+ // once. The post-replay trim then lands exactly on maxEvents.
103
+ const stats = r.replay({ maxEvents: Math.floor(this.maxEvents * 1.2) });
101
104
  this.fileCount = stats.files;
105
+ // Newest-first file read can leave events out of chronological order; restore
106
+ // it (downstream sessions/search/snippet logic assumes ascending ts) and
107
+ // renumber the positional index the BM25 index keys on.
108
+ this.events.sort((a, b) => (a.ts || 0) - (b.ts || 0));
109
+ this.events.forEach((e, k) => { e.i = k; });
102
110
  this.trimEvents();
103
111
  this.rebuildIndex();
104
112
  return stats;