modelstat 0.0.20 → 0.0.21

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/cli.mjs CHANGED
@@ -44503,11 +44503,130 @@ var init_scan = __esm({
44503
44503
  init_pipeline2();
44504
44504
  init_config2();
44505
44505
  init_api();
44506
- AGENT_VERSION = "agent-dev-0.0.20";
44506
+ AGENT_VERSION = "agent-dev-0.0.21";
44507
44507
  BATCH_MAX_EVENTS = 2e3;
44508
44508
  }
44509
44509
  });
44510
44510
 
44511
+ // src/lock.ts
44512
+ import {
44513
+ closeSync,
44514
+ existsSync as existsSync5,
44515
+ mkdirSync as mkdirSync2,
44516
+ openSync,
44517
+ readFileSync as readFileSync2,
44518
+ renameSync,
44519
+ unlinkSync as unlinkSync2,
44520
+ writeFileSync as writeFileSync3,
44521
+ writeSync
44522
+ } from "fs";
44523
+ import { homedir as homedir5 } from "os";
44524
+ import { join as join4 } from "path";
44525
+ function isProcessAlive(pid) {
44526
+ if (!pid || pid <= 0) return false;
44527
+ try {
44528
+ process.kill(pid, 0);
44529
+ return true;
44530
+ } catch (e) {
44531
+ const code = e.code;
44532
+ if (code === "EPERM") return true;
44533
+ return false;
44534
+ }
44535
+ }
44536
+ function readLock() {
44537
+ try {
44538
+ const raw = readFileSync2(LOCK_FILE, "utf8");
44539
+ const obj = JSON.parse(raw);
44540
+ if (typeof obj.pid !== "number") return null;
44541
+ return {
44542
+ pid: obj.pid,
44543
+ startedAt: obj.startedAt ?? "unknown",
44544
+ agentVersion: obj.agentVersion ?? "unknown",
44545
+ apiUrl: obj.apiUrl ?? "unknown"
44546
+ };
44547
+ } catch {
44548
+ return null;
44549
+ }
44550
+ }
44551
+ function writeLockAtomic(meta) {
44552
+ mkdirSync2(LOCK_DIR, { recursive: true });
44553
+ const tmp = `${LOCK_FILE}.${meta.pid}.${Date.now()}.tmp`;
44554
+ const fd = openSync(tmp, "wx");
44555
+ try {
44556
+ writeSync(fd, JSON.stringify(meta, null, 2));
44557
+ } finally {
44558
+ closeSync(fd);
44559
+ }
44560
+ renameSync(tmp, LOCK_FILE);
44561
+ }
44562
+ function removeLockIfOwned(ownerPid) {
44563
+ const lock = readLock();
44564
+ if (!lock) return;
44565
+ if (lock.pid !== ownerPid) return;
44566
+ try {
44567
+ unlinkSync2(LOCK_FILE);
44568
+ } catch {
44569
+ }
44570
+ }
44571
+ function acquireDaemonLock(opts) {
44572
+ const existing = readLock();
44573
+ if (existing && isProcessAlive(existing.pid)) {
44574
+ if (!opts.force) {
44575
+ const ageSec = ageInSeconds(existing.startedAt);
44576
+ return { kind: "already_running", owner: existing, ageSec };
44577
+ }
44578
+ try {
44579
+ process.kill(existing.pid, "SIGTERM");
44580
+ } catch {
44581
+ }
44582
+ }
44583
+ const meta = {
44584
+ pid: process.pid,
44585
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
44586
+ agentVersion: opts.agentVersion,
44587
+ apiUrl: opts.apiUrl
44588
+ };
44589
+ writeLockAtomic(meta);
44590
+ const cleanup = () => removeLockIfOwned(process.pid);
44591
+ process.once("beforeExit", cleanup);
44592
+ process.once("SIGINT", () => {
44593
+ cleanup();
44594
+ process.exit(130);
44595
+ });
44596
+ process.once("SIGTERM", () => {
44597
+ cleanup();
44598
+ process.exit(143);
44599
+ });
44600
+ process.once("uncaughtException", (err) => {
44601
+ cleanup();
44602
+ console.error("modelstat daemon crashed:", err);
44603
+ process.exit(1);
44604
+ });
44605
+ return { kind: "acquired" };
44606
+ }
44607
+ function ageInSeconds(iso) {
44608
+ const t = Date.parse(iso);
44609
+ if (Number.isNaN(t)) return -1;
44610
+ return Math.max(0, Math.floor((Date.now() - t) / 1e3));
44611
+ }
44612
+ function formatAge(seconds) {
44613
+ if (seconds < 0) return "?";
44614
+ if (seconds < 60) return `${seconds}s`;
44615
+ const m = Math.floor(seconds / 60);
44616
+ const s = seconds % 60;
44617
+ if (m < 60) return `${m}m ${s}s`;
44618
+ const h = Math.floor(m / 60);
44619
+ return `${h}h ${m % 60}m`;
44620
+ }
44621
+ var LOCK_DIR, LOCK_FILE;
44622
+ var init_lock = __esm({
44623
+ "src/lock.ts"() {
44624
+ "use strict";
44625
+ LOCK_DIR = join4(homedir5(), ".modelstat");
44626
+ LOCK_FILE = join4(LOCK_DIR, "daemon.lock");
44627
+ }
44628
+ });
44629
+
44511
44630
  // ../../node_modules/.pnpm/readdirp@4.1.2/node_modules/readdirp/esm/index.js
44512
44631
  import { stat as stat3, lstat, readdir as readdir2, realpath } from "fs/promises";
44513
44632
  import { Readable as Readable2 } from "stream";
@@ -46234,7 +46353,7 @@ __export(daemon_exports, {
46234
46353
  setProgress: () => setProgress,
46235
46354
  setQueue: () => setQueue
46236
46355
  });
46237
- import { existsSync as existsSync5, statSync as statSync2 } from "fs";
46356
+ import { existsSync as existsSync6, statSync as statSync2 } from "fs";
46238
46357
  function setPhase(phase, message) {
46239
46358
  status.phase = phase;
46240
46359
  status.message = message ?? null;
@@ -46326,10 +46445,29 @@ async function runScanCycle(reason) {
46326
46445
  function basename3(p) {
46327
46446
  return p.split("/").pop() ?? p;
46328
46447
  }
46329
- async function runDaemon() {
46448
+ async function runDaemon(opts = {}) {
46330
46449
  if (!state.bearer || !state.deviceId) {
46331
46450
  throw new Error("not enrolled \u2014 run `modelstat connect` first");
46332
46451
  }
46452
+ const lock = acquireDaemonLock({
46453
+ agentVersion: AGENT_VERSION2,
46454
+ apiUrl: state.apiUrl,
46455
+ force: opts.force === true
46456
+ });
46457
+ if (lock.kind === "already_running") {
46458
+ console.log(
46459
+ `modelstat daemon is already running \u2014 PID ${lock.owner.pid}, started ${formatAge(
46460
+ lock.ageSec
46461
+ )} ago, agent ${lock.owner.agentVersion}.`
46462
+ );
46463
+ console.log(
46464
+ " \u2192 to stop it: kill " + lock.owner.pid
46465
+ );
46466
+ console.log(
46467
+ " \u2192 to force-replace it: modelstat start --force"
46468
+ );
46469
+ return;
46470
+ }
46333
46471
  setPhase("starting", "Booting");
46334
46472
  const hb = setInterval(() => void sendHeartbeat(), HEARTBEAT_INTERVAL_MS);
46335
46473
  hb.unref();
@@ -46337,21 +46475,21 @@ async function runDaemon() {
46337
46475
  await runDiscovery();
46338
46476
  await runScanCycle("startup");
46339
46477
  const chokidar = (await Promise.resolve().then(() => (init_esm2(), esm_exports))).default;
46340
- const { homedir: homedir6, platform: platform5 } = await import("os");
46341
- const { join: join7 } = await import("path");
46342
- const home2 = homedir6();
46478
+ const { homedir: homedir7, platform: platform5 } = await import("os");
46479
+ const { join: join8 } = await import("path");
46480
+ const home2 = homedir7();
46343
46481
  const dirs = [
46344
- join7(home2, ".claude/projects"),
46345
- join7(home2, ".codex/sessions"),
46346
- join7(home2, ".cursor/ai-tracking"),
46347
- join7(home2, ".gemini"),
46482
+ join8(home2, ".claude/projects"),
46483
+ join8(home2, ".codex/sessions"),
46484
+ join8(home2, ".cursor/ai-tracking"),
46485
+ join8(home2, ".gemini"),
46348
46486
  ...platform5() === "darwin" ? [
46349
- join7(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46350
- join7(home2, "Library/Application Support/Claude")
46487
+ join8(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46488
+ join8(home2, "Library/Application Support/Claude")
46351
46489
  ] : [
46352
- join7(home2, ".config/Cursor/User/workspaceStorage")
46490
+ join8(home2, ".config/Cursor/User/workspaceStorage")
46353
46491
  ]
46354
- ].filter((p) => existsSync5(p) && statSync2(p).isDirectory());
46492
+ ].filter((p) => existsSync6(p) && statSync2(p).isDirectory());
46355
46493
  setPhase("watching", `Watching ${dirs.length} directories`);
46356
46494
  const watcher = chokidar.watch(dirs, {
46357
46495
  persistent: true,
@@ -46396,8 +46534,9 @@ var init_daemon = __esm({
46396
46534
  init_src2();
46397
46535
  init_api();
46398
46536
  init_config2();
46537
+ init_lock();
46399
46538
  init_scan();
46400
- AGENT_VERSION2 = "agent-dev-0.0.20";
46539
+ AGENT_VERSION2 = "agent-dev-0.0.21";
46401
46540
  HEARTBEAT_INTERVAL_MS = 1e4;
46402
46541
  SCAN_INTERVAL_MS = 5 * 60 * 1e3;
46403
46542
  status = {
@@ -46417,37 +46556,37 @@ var watch_exports = {};
46417
46556
  __export(watch_exports, {
46418
46557
  watchForever: () => watchForever
46419
46558
  });
46420
- import { existsSync as existsSync6 } from "fs";
46421
- import { homedir as homedir5, platform as platform3 } from "os";
46422
- import { join as join6 } from "path";
46559
+ import { existsSync as existsSync7 } from "fs";
46560
+ import { homedir as homedir6, platform as platform3 } from "os";
46561
+ import { join as join7 } from "path";
46423
46562
  function resolveWatchDirs() {
46424
- const home2 = homedir5();
46425
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join6(home2, ".config");
46426
- const xdgData = process.env.XDG_DATA_HOME ?? join6(home2, ".local/share");
46563
+ const home2 = homedir6();
46564
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join7(home2, ".config");
46565
+ const xdgData = process.env.XDG_DATA_HOME ?? join7(home2, ".local/share");
46427
46566
  const candidates = [
46428
46567
  // universal (default HOME-rooted CLI data dirs)
46429
- join6(home2, ".claude/projects"),
46430
- join6(home2, ".codex/sessions"),
46431
- join6(home2, ".cursor/ai-tracking"),
46432
- join6(home2, ".gemini"),
46433
- join6(home2, ".aider"),
46568
+ join7(home2, ".claude/projects"),
46569
+ join7(home2, ".codex/sessions"),
46570
+ join7(home2, ".cursor/ai-tracking"),
46571
+ join7(home2, ".gemini"),
46572
+ join7(home2, ".aider"),
46434
46573
  // XDG / Linux
46435
- join6(xdgConfig, "claude/projects"),
46436
- join6(xdgConfig, "codex/sessions"),
46437
- join6(xdgConfig, "Cursor/User/workspaceStorage"),
46438
- join6(xdgConfig, "Code/User/workspaceStorage"),
46439
- join6(xdgConfig, "Code - Insiders/User/workspaceStorage"),
46440
- join6(xdgData, "claude/projects"),
46574
+ join7(xdgConfig, "claude/projects"),
46575
+ join7(xdgConfig, "codex/sessions"),
46576
+ join7(xdgConfig, "Cursor/User/workspaceStorage"),
46577
+ join7(xdgConfig, "Code/User/workspaceStorage"),
46578
+ join7(xdgConfig, "Code - Insiders/User/workspaceStorage"),
46579
+ join7(xdgData, "claude/projects"),
46441
46580
  // macOS
46442
46581
  ...platform3() === "darwin" ? [
46443
- join6(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46444
- join6(home2, "Library/Application Support/Claude"),
46445
- join6(home2, "Library/Application Support/Code/User/workspaceStorage"),
46446
- join6(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
46447
- join6(home2, "Library/Application Support/Zed")
46582
+ join7(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46583
+ join7(home2, "Library/Application Support/Claude"),
46584
+ join7(home2, "Library/Application Support/Code/User/workspaceStorage"),
46585
+ join7(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
46586
+ join7(home2, "Library/Application Support/Zed")
46448
46587
  ] : []
46449
46588
  ];
46450
- return Array.from(new Set(candidates)).filter((p) => existsSync6(p));
46589
+ return Array.from(new Set(candidates)).filter((p) => existsSync7(p));
46451
46590
  }
46452
46591
  async function safeScan(reason) {
46453
46592
  if (scanning) {
@@ -47063,13 +47202,14 @@ async function cmdWatch() {
47063
47202
  const { watchForever: watchForever2 } = await Promise.resolve().then(() => (init_watch(), watch_exports));
47064
47203
  await watchForever2();
47065
47204
  }
47066
- async function cmdStart() {
47205
+ async function cmdStart(rest) {
47067
47206
  if (!state.bearer || !state.deviceId) {
47068
47207
  console.error("not paired yet. Run `modelstat connect` first.");
47069
47208
  process.exit(1);
47070
47209
  }
47210
+ const force = rest.includes("--force") || rest.includes("-f");
47071
47211
  const { runDaemon: runDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
47072
- await runDaemon2();
47212
+ await runDaemon2({ force });
47073
47213
  }
47074
47214
  async function cmdStop() {
47075
47215
  try {
@@ -47233,7 +47373,7 @@ async function main() {
47233
47373
  case "start":
47234
47374
  if (!state.bearer || !state.deviceId)
47235
47375
  return cmdConnect(parseConnectOpts(rest));
47236
- return cmdStart();
47376
+ return cmdStart(rest);
47237
47377
  case "connect":
47238
47378
  return cmdConnect(parseConnectOpts(rest));
47239
47379
  case "self-register":