modelstat 0.0.33 → 0.0.35

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": "modelstat",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "description": "modelstat companion — reads local AI-tool usage and ships tokenised events to modelstat.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -15,19 +15,6 @@
15
15
  "README.md",
16
16
  "LICENSE"
17
17
  ],
18
- "scripts": {
19
- "dev:connect": "tsx ./src/cli.ts connect",
20
- "dev:scan": "tsx ./src/cli.ts scan",
21
- "dev:watch": "tsx ./src/cli.ts watch",
22
- "dev:discover": "tsx ./src/cli.ts discover",
23
- "build": "tsup",
24
- "build:tray": "bash ./scripts/build-tray.sh",
25
- "prepack": "pnpm run build && pnpm run build:tray",
26
- "pack:tarball": "pnpm run build && npm pack --pack-destination ../..",
27
- "install:local": "bash ./scripts/install-local.sh",
28
- "postinstall": "node ./scripts/postinstall.mjs",
29
- "typecheck": "tsc --noEmit"
30
- },
31
18
  "dependencies": {
32
19
  "chokidar": "^4.0.3",
33
20
  "conf": "^13.1.0",
@@ -37,13 +24,13 @@
37
24
  "undici": "^7.1.0"
38
25
  },
39
26
  "devDependencies": {
40
- "@modelstat/companion-core": "workspace:*",
41
- "@modelstat/core": "workspace:*",
42
- "@modelstat/parsers": "workspace:*",
43
27
  "@types/node": "^22.10.5",
44
28
  "tsup": "^8.3.5",
45
29
  "tsx": "^4.19.2",
46
- "typescript": "^5.7.3"
30
+ "typescript": "^5.7.3",
31
+ "@modelstat/companion-core": "0.0.0",
32
+ "@modelstat/core": "0.0.0",
33
+ "@modelstat/parsers": "0.0.0"
47
34
  },
48
35
  "engines": {
49
36
  "node": ">=20.18.0"
@@ -71,5 +58,17 @@
71
58
  "type": "git",
72
59
  "url": "git+https://github.com/modelstat/modelstat.git",
73
60
  "directory": "apps/agent-dev"
61
+ },
62
+ "scripts": {
63
+ "dev:connect": "tsx ./src/cli.ts connect",
64
+ "dev:scan": "tsx ./src/cli.ts scan",
65
+ "dev:watch": "tsx ./src/cli.ts watch",
66
+ "dev:discover": "tsx ./src/cli.ts discover",
67
+ "build": "tsup",
68
+ "build:tray": "bash ./scripts/build-tray.sh",
69
+ "pack:tarball": "pnpm run build && npm pack --pack-destination ../..",
70
+ "install:local": "bash ./scripts/install-local.sh",
71
+ "postinstall": "node ./scripts/postinstall.mjs",
72
+ "typecheck": "tsc --noEmit"
74
73
  }
75
- }
74
+ }
@@ -317,13 +317,71 @@ async function setupNativeNodeModules(installedBundle) {
317
317
  }
318
318
 
319
319
  /**
320
- * Kill any daemon process the service supervisor didn't reap.
321
- * Reads ~/.modelstat/daemon.lock for the PID, sends SIGTERM, then
322
- * SIGKILL after a short grace period. Tolerates a missing/stale
323
- * lock file we just want to make sure the new bundle is the only
324
- * thing holding the lock when we restart.
320
+ * Kill any modelstat-related process the service supervisor didn't
321
+ * reap. Two passes:
322
+ * (1) PID from ~/.modelstat/daemon.lock the most reliable hit
323
+ * when the lock is fresh and our own daemon wrote it.
324
+ * (2) Process scan via `pgrep -f` on macOS/Linux — catches stray
325
+ * daemons launched by an OLDER bundle (different command
326
+ * line, different lock format), zombies that survived a
327
+ * launchd KeepAlive bounce, and the case where the user has
328
+ * BOTH the launchd-managed daemon AND a hand-spawned one
329
+ * running. The pattern is intentionally narrow
330
+ * (`modelstat\\.mjs`) so we don't kill `modelstatd` or any
331
+ * other process that happens to share a substring.
332
+ *
333
+ * SIGTERM first, escalate to SIGKILL after 2 s. All failures
334
+ * tolerated — we just want the new bundle to be the only modelstat
335
+ * process holding the lock when we restart.
325
336
  */
326
337
  async function killStaleDaemon(stateDir) {
338
+ await killByLockfile(stateDir);
339
+ await killByProcessScan();
340
+ }
341
+
342
+ async function killByProcessScan() {
343
+ const { spawnSync } = await import("node:child_process");
344
+ // pgrep is on macOS by default, Linux via procps.
345
+ const r = spawnSync("pgrep", ["-f", "modelstat\\.mjs"], {
346
+ encoding: "utf8",
347
+ });
348
+ if (r.status !== 0 || !r.stdout) return;
349
+ const pids = r.stdout
350
+ .split(/\s+/)
351
+ .map((s) => Number(s))
352
+ .filter((n) => Number.isInteger(n) && n > 0 && n !== process.pid);
353
+ if (pids.length === 0) return;
354
+ for (const pid of pids) {
355
+ try {
356
+ process.kill(pid, "SIGTERM");
357
+ } catch {
358
+ /* gone or not ours */
359
+ }
360
+ }
361
+ // Wait up to 2 s for graceful exits.
362
+ for (let i = 0; i < 20; i++) {
363
+ let alive = 0;
364
+ for (const pid of pids) {
365
+ try {
366
+ process.kill(pid, 0);
367
+ alive += 1;
368
+ } catch {
369
+ /* exited */
370
+ }
371
+ }
372
+ if (alive === 0) return;
373
+ await new Promise((r) => setTimeout(r, 100));
374
+ }
375
+ for (const pid of pids) {
376
+ try {
377
+ process.kill(pid, "SIGKILL");
378
+ } catch {
379
+ /* gone or denied */
380
+ }
381
+ }
382
+ }
383
+
384
+ async function killByLockfile(stateDir) {
327
385
  const { readFileSync } = await import("node:fs");
328
386
  const lockPath = join(stateDir, "daemon.lock");
329
387
  let payload;
File without changes