@ohmaseclaro/fleetwatch 0.1.6 → 0.1.7
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/dist/server/watcher.js +90 -0
- package/package.json +1 -1
package/dist/server/watcher.js
CHANGED
|
@@ -108,6 +108,10 @@ export class Watcher extends BaseProvider {
|
|
|
108
108
|
historyState = null;
|
|
109
109
|
projectByPath = new Map();
|
|
110
110
|
excludePathPrefixes;
|
|
111
|
+
/** Polling reconciliation timer — catches fs events chokidar misses. */
|
|
112
|
+
pollTimer = null;
|
|
113
|
+
/** Directory rescan timer — catches new session files chokidar misses. */
|
|
114
|
+
rescanTimer = null;
|
|
111
115
|
constructor(opts) {
|
|
112
116
|
super(opts);
|
|
113
117
|
this.excludePathPrefixes = opts.excludePathPrefixes ?? [];
|
|
@@ -249,16 +253,102 @@ export class Watcher extends BaseProvider {
|
|
|
249
253
|
}
|
|
250
254
|
// 1s tick for status recompute (handles idle / awaiting-user transitions)
|
|
251
255
|
setInterval(() => this.registry.recomputeAll(), 1000).unref();
|
|
256
|
+
// 2s tick: poll tracked files. chokidar / fsevents on macOS sometimes
|
|
257
|
+
// misses appends on deep directory trees or when editors write via atomic
|
|
258
|
+
// rename. This is a cheap safety net — just an `fs.stat` per file and a
|
|
259
|
+
// size/inode comparison. Catches missed change events without re-reading
|
|
260
|
+
// any data unless something actually changed.
|
|
261
|
+
this.pollTimer = setInterval(() => this.reconcileTrackedFiles(), 2000);
|
|
262
|
+
this.pollTimer.unref?.();
|
|
263
|
+
// 10s tick: scan watched directories for NEW *.jsonl files chokidar's
|
|
264
|
+
// `add` event might have missed. Less frequent because directory walks
|
|
265
|
+
// are heavier than per-file stats.
|
|
266
|
+
this.rescanTimer = setInterval(() => this.rescanWatchedDirs(), 10_000);
|
|
267
|
+
this.rescanTimer.unref?.();
|
|
252
268
|
// If we found absolutely nothing, surface that clearly to the operator.
|
|
253
269
|
if (!this.claudeDir && !this.coworkDir) {
|
|
254
270
|
this.skipStartup("no Claude or Cowork data found in any candidate location");
|
|
255
271
|
}
|
|
256
272
|
}
|
|
257
273
|
async onStop() {
|
|
274
|
+
if (this.pollTimer) {
|
|
275
|
+
clearInterval(this.pollTimer);
|
|
276
|
+
this.pollTimer = null;
|
|
277
|
+
}
|
|
278
|
+
if (this.rescanTimer) {
|
|
279
|
+
clearInterval(this.rescanTimer);
|
|
280
|
+
this.rescanTimer = null;
|
|
281
|
+
}
|
|
258
282
|
await this.projectWatcher?.close();
|
|
259
283
|
await this.coworkWatcher?.close();
|
|
260
284
|
await this.historyWatcher?.close();
|
|
261
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Stat every tracked file and re-read any whose on-disk size or inode has
|
|
288
|
+
* drifted past our cached TailState. Catches `change` events chokidar misses
|
|
289
|
+
* (macOS fsevents on deep directories, atomic-rename writes, etc.).
|
|
290
|
+
*/
|
|
291
|
+
async reconcileTrackedFiles() {
|
|
292
|
+
if (this.files.size === 0)
|
|
293
|
+
return;
|
|
294
|
+
// Snapshot entries up front because onFileChange may mutate this.files.
|
|
295
|
+
const entries = Array.from(this.files.values());
|
|
296
|
+
for (const entry of entries) {
|
|
297
|
+
if (entry.pending || entry.queued)
|
|
298
|
+
continue;
|
|
299
|
+
if (!entry.state)
|
|
300
|
+
continue; // not yet initially-consumed; chokidar add will handle it
|
|
301
|
+
let st = null;
|
|
302
|
+
try {
|
|
303
|
+
st = (await fs.stat(entry.filePath));
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// File vanished — chokidar's unlink handler will clean up; skip.
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (!st)
|
|
310
|
+
continue;
|
|
311
|
+
const rotated = st.ino !== entry.state.inode;
|
|
312
|
+
const grew = st.size > entry.state.offset;
|
|
313
|
+
const shrank = st.size < entry.state.offset;
|
|
314
|
+
if (rotated || grew || shrank) {
|
|
315
|
+
// Reuse the normal change pipeline so dedupe / queueing still apply.
|
|
316
|
+
this.onFileChange(entry.filePath, entry.source, "poll").catch((err) => {
|
|
317
|
+
this.log(`[watcher poll] ${entry.filePath}: ${err.message}`);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Walk the watched roots for any *.jsonl files we don't already have in
|
|
324
|
+
* `this.files`. Catches `add` events chokidar misses.
|
|
325
|
+
*/
|
|
326
|
+
async rescanWatchedDirs() {
|
|
327
|
+
const roots = [];
|
|
328
|
+
if (this.claudeDir)
|
|
329
|
+
roots.push([this.claudeDir, "claude-code"]);
|
|
330
|
+
if (this.coworkDir)
|
|
331
|
+
roots.push([this.coworkDir, "cowork"]);
|
|
332
|
+
for (const [root, source] of roots) {
|
|
333
|
+
const found = [];
|
|
334
|
+
try {
|
|
335
|
+
await walk(root, found);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
for (const file of found) {
|
|
341
|
+
if (!file.endsWith(".jsonl"))
|
|
342
|
+
continue;
|
|
343
|
+
if (this.files.has(file))
|
|
344
|
+
continue;
|
|
345
|
+
// New file we missed — feed it through the normal pipeline.
|
|
346
|
+
this.onFileChange(file, source, "rescan").catch((err) => {
|
|
347
|
+
this.log(`[watcher rescan] ${file}: ${err.message}`);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
262
352
|
async initialScan(root, source) {
|
|
263
353
|
const found = [];
|
|
264
354
|
await walk(root, found);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ohmaseclaro/fleetwatch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Watch every Claude Code, Cowork, and Cursor session from your phone — multi-provider agent observability with live updates over LAN or ngrok.",
|
|
6
6
|
"keywords": [
|