modelstat 0.0.20 → 0.0.22

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
@@ -4341,7 +4341,14 @@ var init_schemas = __esm({
4341
4341
  files_touched: external_exports.array(external_exports.string().max(512)).max(256).default([]),
4342
4342
  // Reference to originating file for reparsing
4343
4343
  source_file: external_exports.string().max(1024).nullable(),
4344
- source_byte_offset: external_exports.number().int().nonnegative().nullable()
4344
+ source_byte_offset: external_exports.number().int().nonnegative().nullable(),
4345
+ // Billing mode. Tools with a flat-fee subscription tier (Claude
4346
+ // Code, Cursor Pro, GitHub Copilot, etc) emit events tagged
4347
+ // `billing: "subscription"` — the server short-circuits cost to $0
4348
+ // for those, since token-level pricing doesn't apply once the user
4349
+ // is paying the subscription. `api` (or absent) means pay-per-token
4350
+ // against whatever rates the org has configured.
4351
+ billing: external_exports.enum(["subscription", "api"]).optional()
4345
4352
  });
4346
4353
  RedactionReport = external_exports.object({
4347
4354
  secrets_found: external_exports.number().int().nonnegative().default(0),
@@ -4723,7 +4730,12 @@ async function parseClaudeCodeJsonl(ctx) {
4723
4730
  tool_calls: {},
4724
4731
  files_touched: [],
4725
4732
  source_file: ctx.sourceFile,
4726
- source_byte_offset: offsetAtLineStart
4733
+ source_byte_offset: offsetAtLineStart,
4734
+ // Files in ~/.claude/projects/ come from the Claude Code app
4735
+ // used via subscription (not the raw API). Mark them so the
4736
+ // server short-circuits token-level cost to $0 — the user has
4737
+ // already paid the flat monthly fee.
4738
+ billing: "subscription"
4727
4739
  });
4728
4740
  } else if (obj.type === "user") {
4729
4741
  const u = obj;
@@ -4748,7 +4760,8 @@ async function parseClaudeCodeJsonl(ctx) {
4748
4760
  tool_calls: {},
4749
4761
  files_touched: [],
4750
4762
  source_file: ctx.sourceFile,
4751
- source_byte_offset: offsetAtLineStart
4763
+ source_byte_offset: offsetAtLineStart,
4764
+ billing: "subscription"
4752
4765
  });
4753
4766
  } else {
4754
4767
  skipped += 1;
@@ -44503,11 +44516,130 @@ var init_scan = __esm({
44503
44516
  init_pipeline2();
44504
44517
  init_config2();
44505
44518
  init_api();
44506
- AGENT_VERSION = "agent-dev-0.0.20";
44519
+ AGENT_VERSION = "agent-dev-0.0.22";
44507
44520
  BATCH_MAX_EVENTS = 2e3;
44508
44521
  }
44509
44522
  });
44510
44523
 
44524
+ // src/lock.ts
44525
+ import {
44526
+ closeSync,
44527
+ existsSync as existsSync5,
44528
+ mkdirSync as mkdirSync2,
44529
+ openSync,
44530
+ readFileSync as readFileSync2,
44531
+ renameSync,
44532
+ unlinkSync as unlinkSync2,
44533
+ writeFileSync as writeFileSync3,
44534
+ writeSync
44535
+ } from "fs";
44536
+ import { homedir as homedir5 } from "os";
44537
+ import { join as join4 } from "path";
44538
+ function isProcessAlive(pid) {
44539
+ if (!pid || pid <= 0) return false;
44540
+ try {
44541
+ process.kill(pid, 0);
44542
+ return true;
44543
+ } catch (e) {
44544
+ const code = e.code;
44545
+ if (code === "EPERM") return true;
44546
+ return false;
44547
+ }
44548
+ }
44549
+ function readLock() {
44550
+ try {
44551
+ const raw = readFileSync2(LOCK_FILE, "utf8");
44552
+ const obj = JSON.parse(raw);
44553
+ if (typeof obj.pid !== "number") return null;
44554
+ return {
44555
+ pid: obj.pid,
44556
+ startedAt: obj.startedAt ?? "unknown",
44557
+ agentVersion: obj.agentVersion ?? "unknown",
44558
+ apiUrl: obj.apiUrl ?? "unknown"
44559
+ };
44560
+ } catch {
44561
+ return null;
44562
+ }
44563
+ }
44564
+ function writeLockAtomic(meta) {
44565
+ mkdirSync2(LOCK_DIR, { recursive: true });
44566
+ const tmp = `${LOCK_FILE}.${meta.pid}.${Date.now()}.tmp`;
44567
+ const fd = openSync(tmp, "wx");
44568
+ try {
44569
+ writeSync(fd, JSON.stringify(meta, null, 2));
44570
+ } finally {
44571
+ closeSync(fd);
44572
+ }
44573
+ renameSync(tmp, LOCK_FILE);
44574
+ }
44575
+ function removeLockIfOwned(ownerPid) {
44576
+ const lock = readLock();
44577
+ if (!lock) return;
44578
+ if (lock.pid !== ownerPid) return;
44579
+ try {
44580
+ unlinkSync2(LOCK_FILE);
44581
+ } catch {
44582
+ }
44583
+ }
44584
+ function acquireDaemonLock(opts) {
44585
+ const existing = readLock();
44586
+ if (existing && isProcessAlive(existing.pid)) {
44587
+ if (!opts.force) {
44588
+ const ageSec = ageInSeconds(existing.startedAt);
44589
+ return { kind: "already_running", owner: existing, ageSec };
44590
+ }
44591
+ try {
44592
+ process.kill(existing.pid, "SIGTERM");
44593
+ } catch {
44594
+ }
44595
+ }
44596
+ const meta = {
44597
+ pid: process.pid,
44598
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
44599
+ agentVersion: opts.agentVersion,
44600
+ apiUrl: opts.apiUrl
44601
+ };
44602
+ writeLockAtomic(meta);
44603
+ const cleanup = () => removeLockIfOwned(process.pid);
44604
+ process.once("beforeExit", cleanup);
44605
+ process.once("SIGINT", () => {
44606
+ cleanup();
44607
+ process.exit(130);
44608
+ });
44609
+ process.once("SIGTERM", () => {
44610
+ cleanup();
44611
+ process.exit(143);
44612
+ });
44613
+ process.once("uncaughtException", (err) => {
44614
+ cleanup();
44615
+ console.error("modelstat daemon crashed:", err);
44616
+ process.exit(1);
44617
+ });
44618
+ return { kind: "acquired" };
44619
+ }
44620
+ function ageInSeconds(iso) {
44621
+ const t = Date.parse(iso);
44622
+ if (Number.isNaN(t)) return -1;
44623
+ return Math.max(0, Math.floor((Date.now() - t) / 1e3));
44624
+ }
44625
+ function formatAge(seconds) {
44626
+ if (seconds < 0) return "?";
44627
+ if (seconds < 60) return `${seconds}s`;
44628
+ const m = Math.floor(seconds / 60);
44629
+ const s = seconds % 60;
44630
+ if (m < 60) return `${m}m ${s}s`;
44631
+ const h = Math.floor(m / 60);
44632
+ return `${h}h ${m % 60}m`;
44633
+ }
44634
+ var LOCK_DIR, LOCK_FILE;
44635
+ var init_lock = __esm({
44636
+ "src/lock.ts"() {
44637
+ "use strict";
44638
+ LOCK_DIR = join4(homedir5(), ".modelstat");
44639
+ LOCK_FILE = join4(LOCK_DIR, "daemon.lock");
44640
+ }
44641
+ });
44642
+
44511
44643
  // ../../node_modules/.pnpm/readdirp@4.1.2/node_modules/readdirp/esm/index.js
44512
44644
  import { stat as stat3, lstat, readdir as readdir2, realpath } from "fs/promises";
44513
44645
  import { Readable as Readable2 } from "stream";
@@ -46234,7 +46366,7 @@ __export(daemon_exports, {
46234
46366
  setProgress: () => setProgress,
46235
46367
  setQueue: () => setQueue
46236
46368
  });
46237
- import { existsSync as existsSync5, statSync as statSync2 } from "fs";
46369
+ import { existsSync as existsSync6, statSync as statSync2 } from "fs";
46238
46370
  function setPhase(phase, message) {
46239
46371
  status.phase = phase;
46240
46372
  status.message = message ?? null;
@@ -46326,10 +46458,29 @@ async function runScanCycle(reason) {
46326
46458
  function basename3(p) {
46327
46459
  return p.split("/").pop() ?? p;
46328
46460
  }
46329
- async function runDaemon() {
46461
+ async function runDaemon(opts = {}) {
46330
46462
  if (!state.bearer || !state.deviceId) {
46331
46463
  throw new Error("not enrolled \u2014 run `modelstat connect` first");
46332
46464
  }
46465
+ const lock = acquireDaemonLock({
46466
+ agentVersion: AGENT_VERSION2,
46467
+ apiUrl: state.apiUrl,
46468
+ force: opts.force === true
46469
+ });
46470
+ if (lock.kind === "already_running") {
46471
+ console.log(
46472
+ `modelstat daemon is already running \u2014 PID ${lock.owner.pid}, started ${formatAge(
46473
+ lock.ageSec
46474
+ )} ago, agent ${lock.owner.agentVersion}.`
46475
+ );
46476
+ console.log(
46477
+ " \u2192 to stop it: kill " + lock.owner.pid
46478
+ );
46479
+ console.log(
46480
+ " \u2192 to force-replace it: modelstat start --force"
46481
+ );
46482
+ return;
46483
+ }
46333
46484
  setPhase("starting", "Booting");
46334
46485
  const hb = setInterval(() => void sendHeartbeat(), HEARTBEAT_INTERVAL_MS);
46335
46486
  hb.unref();
@@ -46337,21 +46488,21 @@ async function runDaemon() {
46337
46488
  await runDiscovery();
46338
46489
  await runScanCycle("startup");
46339
46490
  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();
46491
+ const { homedir: homedir7, platform: platform5 } = await import("os");
46492
+ const { join: join8 } = await import("path");
46493
+ const home2 = homedir7();
46343
46494
  const dirs = [
46344
- join7(home2, ".claude/projects"),
46345
- join7(home2, ".codex/sessions"),
46346
- join7(home2, ".cursor/ai-tracking"),
46347
- join7(home2, ".gemini"),
46495
+ join8(home2, ".claude/projects"),
46496
+ join8(home2, ".codex/sessions"),
46497
+ join8(home2, ".cursor/ai-tracking"),
46498
+ join8(home2, ".gemini"),
46348
46499
  ...platform5() === "darwin" ? [
46349
- join7(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46350
- join7(home2, "Library/Application Support/Claude")
46500
+ join8(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46501
+ join8(home2, "Library/Application Support/Claude")
46351
46502
  ] : [
46352
- join7(home2, ".config/Cursor/User/workspaceStorage")
46503
+ join8(home2, ".config/Cursor/User/workspaceStorage")
46353
46504
  ]
46354
- ].filter((p) => existsSync5(p) && statSync2(p).isDirectory());
46505
+ ].filter((p) => existsSync6(p) && statSync2(p).isDirectory());
46355
46506
  setPhase("watching", `Watching ${dirs.length} directories`);
46356
46507
  const watcher = chokidar.watch(dirs, {
46357
46508
  persistent: true,
@@ -46396,8 +46547,9 @@ var init_daemon = __esm({
46396
46547
  init_src2();
46397
46548
  init_api();
46398
46549
  init_config2();
46550
+ init_lock();
46399
46551
  init_scan();
46400
- AGENT_VERSION2 = "agent-dev-0.0.20";
46552
+ AGENT_VERSION2 = "agent-dev-0.0.22";
46401
46553
  HEARTBEAT_INTERVAL_MS = 1e4;
46402
46554
  SCAN_INTERVAL_MS = 5 * 60 * 1e3;
46403
46555
  status = {
@@ -46417,37 +46569,37 @@ var watch_exports = {};
46417
46569
  __export(watch_exports, {
46418
46570
  watchForever: () => watchForever
46419
46571
  });
46420
- import { existsSync as existsSync6 } from "fs";
46421
- import { homedir as homedir5, platform as platform3 } from "os";
46422
- import { join as join6 } from "path";
46572
+ import { existsSync as existsSync7 } from "fs";
46573
+ import { homedir as homedir6, platform as platform3 } from "os";
46574
+ import { join as join7 } from "path";
46423
46575
  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");
46576
+ const home2 = homedir6();
46577
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join7(home2, ".config");
46578
+ const xdgData = process.env.XDG_DATA_HOME ?? join7(home2, ".local/share");
46427
46579
  const candidates = [
46428
46580
  // 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"),
46581
+ join7(home2, ".claude/projects"),
46582
+ join7(home2, ".codex/sessions"),
46583
+ join7(home2, ".cursor/ai-tracking"),
46584
+ join7(home2, ".gemini"),
46585
+ join7(home2, ".aider"),
46434
46586
  // 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"),
46587
+ join7(xdgConfig, "claude/projects"),
46588
+ join7(xdgConfig, "codex/sessions"),
46589
+ join7(xdgConfig, "Cursor/User/workspaceStorage"),
46590
+ join7(xdgConfig, "Code/User/workspaceStorage"),
46591
+ join7(xdgConfig, "Code - Insiders/User/workspaceStorage"),
46592
+ join7(xdgData, "claude/projects"),
46441
46593
  // macOS
46442
46594
  ...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")
46595
+ join7(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46596
+ join7(home2, "Library/Application Support/Claude"),
46597
+ join7(home2, "Library/Application Support/Code/User/workspaceStorage"),
46598
+ join7(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
46599
+ join7(home2, "Library/Application Support/Zed")
46448
46600
  ] : []
46449
46601
  ];
46450
- return Array.from(new Set(candidates)).filter((p) => existsSync6(p));
46602
+ return Array.from(new Set(candidates)).filter((p) => existsSync7(p));
46451
46603
  }
46452
46604
  async function safeScan(reason) {
46453
46605
  if (scanning) {
@@ -47063,13 +47215,14 @@ async function cmdWatch() {
47063
47215
  const { watchForever: watchForever2 } = await Promise.resolve().then(() => (init_watch(), watch_exports));
47064
47216
  await watchForever2();
47065
47217
  }
47066
- async function cmdStart() {
47218
+ async function cmdStart(rest) {
47067
47219
  if (!state.bearer || !state.deviceId) {
47068
47220
  console.error("not paired yet. Run `modelstat connect` first.");
47069
47221
  process.exit(1);
47070
47222
  }
47223
+ const force = rest.includes("--force") || rest.includes("-f");
47071
47224
  const { runDaemon: runDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
47072
- await runDaemon2();
47225
+ await runDaemon2({ force });
47073
47226
  }
47074
47227
  async function cmdStop() {
47075
47228
  try {
@@ -47233,7 +47386,7 @@ async function main() {
47233
47386
  case "start":
47234
47387
  if (!state.bearer || !state.deviceId)
47235
47388
  return cmdConnect(parseConnectOpts(rest));
47236
- return cmdStart();
47389
+ return cmdStart(rest);
47237
47390
  case "connect":
47238
47391
  return cmdConnect(parseConnectOpts(rest));
47239
47392
  case "self-register":