modelstat 0.0.19 → 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
@@ -44049,7 +44049,15 @@ async function buildForOneSession(sessionId, events, adapters2) {
44049
44049
  const turnSurfaces = sorted.map((e) => turnSurface(e));
44050
44050
  const turnEmbeddings = [];
44051
44051
  for (const s of turnSurfaces) {
44052
- turnEmbeddings.push(s.length > 0 ? await adapters2.embed(s) : []);
44052
+ if (s.length === 0) {
44053
+ turnEmbeddings.push([]);
44054
+ continue;
44055
+ }
44056
+ try {
44057
+ turnEmbeddings.push(await adapters2.embed(s));
44058
+ } catch {
44059
+ turnEmbeddings.push([]);
44060
+ }
44053
44061
  }
44054
44062
  const boundaries = [];
44055
44063
  let runStart = 0;
@@ -44320,27 +44328,62 @@ var init_node2 = __esm({
44320
44328
  });
44321
44329
 
44322
44330
  // src/pipeline.ts
44323
- function getAdapters() {
44324
- if (!adapters) {
44325
- const cfg = defaultOllamaConfig();
44331
+ async function probeOllama(baseUrl) {
44332
+ try {
44333
+ const ctrl = new AbortController();
44334
+ const t = setTimeout(() => ctrl.abort(), 500);
44335
+ const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/tags`, {
44336
+ method: "GET",
44337
+ signal: ctrl.signal
44338
+ });
44339
+ clearTimeout(t);
44340
+ return res.ok;
44341
+ } catch {
44342
+ return false;
44343
+ }
44344
+ }
44345
+ function fallbackAdapters() {
44346
+ return {
44347
+ embed: async () => [],
44348
+ // Throw → companion-core/summariseSlice's try/catch falls back to
44349
+ // the `promptFacts` string which is what we want here.
44350
+ summarize: async () => {
44351
+ throw new Error("ollama_unavailable");
44352
+ },
44353
+ tokenize: (text) => Math.max(1, Math.ceil(text.length / 4))
44354
+ };
44355
+ }
44356
+ async function getAdapters() {
44357
+ if (adapters && probed) return adapters;
44358
+ const cfg = defaultOllamaConfig();
44359
+ const up = await probeOllama(cfg.baseUrl);
44360
+ probed = true;
44361
+ if (up) {
44362
+ console.log(`[modelstat] ollama up at ${cfg.baseUrl} \u2014 using LLM pipeline`);
44326
44363
  adapters = {
44327
44364
  embed: ollamaEmbed(cfg),
44328
44365
  summarize: ollamaSummarize(cfg),
44329
44366
  tokenize: ollamaTokenize()
44330
44367
  };
44368
+ } else {
44369
+ console.log(
44370
+ `[modelstat] ollama not reachable at ${cfg.baseUrl} \u2014 using fallback pipeline (server does classification)`
44371
+ );
44372
+ adapters = fallbackAdapters();
44331
44373
  }
44332
44374
  return adapters;
44333
44375
  }
44334
44376
  async function buildSegments(events) {
44335
- return buildSegmentsForSession(events, getAdapters());
44377
+ return buildSegmentsForSession(events, await getAdapters());
44336
44378
  }
44337
- var adapters;
44379
+ var adapters, probed;
44338
44380
  var init_pipeline2 = __esm({
44339
44381
  "src/pipeline.ts"() {
44340
44382
  "use strict";
44341
44383
  init_pipeline();
44342
44384
  init_node2();
44343
44385
  adapters = null;
44386
+ probed = false;
44344
44387
  }
44345
44388
  });
44346
44389
 
@@ -44406,6 +44449,7 @@ async function scanAll(cb = {}) {
44406
44449
  let batchesUploaded = 0;
44407
44450
  let eventsUploaded = 0;
44408
44451
  let buffer = [];
44452
+ let pendingCursors = [];
44409
44453
  async function flushBatch() {
44410
44454
  if (!buffer.length) return;
44411
44455
  cb.onUpload?.(buffer.length);
@@ -44420,6 +44464,8 @@ async function scanAll(cb = {}) {
44420
44464
  const res = await uploadBatch(batch);
44421
44465
  batchesUploaded += 1;
44422
44466
  eventsUploaded += res.accepted;
44467
+ for (const pc of pendingCursors) state.setCursor(pc.path, pc.cs);
44468
+ pendingCursors = [];
44423
44469
  buffer = [];
44424
44470
  }
44425
44471
  for (let i = 0; i < jobs.length; i++) {
@@ -44440,7 +44486,7 @@ async function scanAll(cb = {}) {
44440
44486
  if (buffer.length >= BATCH_MAX_EVENTS) await flushBatch();
44441
44487
  }
44442
44488
  }
44443
- if (cs) state.setCursor(job.path, cs);
44489
+ if (cs) pendingCursors.push({ path: job.path, cs });
44444
44490
  } catch (e) {
44445
44491
  console.warn(` ! parse failed for ${job.path}:`, e.message);
44446
44492
  }
@@ -44457,11 +44503,130 @@ var init_scan = __esm({
44457
44503
  init_pipeline2();
44458
44504
  init_config2();
44459
44505
  init_api();
44460
- AGENT_VERSION = "agent-dev-0.0.1";
44506
+ AGENT_VERSION = "agent-dev-0.0.21";
44461
44507
  BATCH_MAX_EVENTS = 2e3;
44462
44508
  }
44463
44509
  });
44464
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
+
44465
44630
  // ../../node_modules/.pnpm/readdirp@4.1.2/node_modules/readdirp/esm/index.js
44466
44631
  import { stat as stat3, lstat, readdir as readdir2, realpath } from "fs/promises";
44467
44632
  import { Readable as Readable2 } from "stream";
@@ -46188,7 +46353,7 @@ __export(daemon_exports, {
46188
46353
  setProgress: () => setProgress,
46189
46354
  setQueue: () => setQueue
46190
46355
  });
46191
- import { existsSync as existsSync5, statSync as statSync2 } from "fs";
46356
+ import { existsSync as existsSync6, statSync as statSync2 } from "fs";
46192
46357
  function setPhase(phase, message) {
46193
46358
  status.phase = phase;
46194
46359
  status.message = message ?? null;
@@ -46280,10 +46445,29 @@ async function runScanCycle(reason) {
46280
46445
  function basename3(p) {
46281
46446
  return p.split("/").pop() ?? p;
46282
46447
  }
46283
- async function runDaemon() {
46448
+ async function runDaemon(opts = {}) {
46284
46449
  if (!state.bearer || !state.deviceId) {
46285
46450
  throw new Error("not enrolled \u2014 run `modelstat connect` first");
46286
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
+ }
46287
46471
  setPhase("starting", "Booting");
46288
46472
  const hb = setInterval(() => void sendHeartbeat(), HEARTBEAT_INTERVAL_MS);
46289
46473
  hb.unref();
@@ -46291,21 +46475,21 @@ async function runDaemon() {
46291
46475
  await runDiscovery();
46292
46476
  await runScanCycle("startup");
46293
46477
  const chokidar = (await Promise.resolve().then(() => (init_esm2(), esm_exports))).default;
46294
- const { homedir: homedir6, platform: platform5 } = await import("os");
46295
- const { join: join7 } = await import("path");
46296
- const home2 = homedir6();
46478
+ const { homedir: homedir7, platform: platform5 } = await import("os");
46479
+ const { join: join8 } = await import("path");
46480
+ const home2 = homedir7();
46297
46481
  const dirs = [
46298
- join7(home2, ".claude/projects"),
46299
- join7(home2, ".codex/sessions"),
46300
- join7(home2, ".cursor/ai-tracking"),
46301
- join7(home2, ".gemini"),
46482
+ join8(home2, ".claude/projects"),
46483
+ join8(home2, ".codex/sessions"),
46484
+ join8(home2, ".cursor/ai-tracking"),
46485
+ join8(home2, ".gemini"),
46302
46486
  ...platform5() === "darwin" ? [
46303
- join7(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46304
- join7(home2, "Library/Application Support/Claude")
46487
+ join8(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46488
+ join8(home2, "Library/Application Support/Claude")
46305
46489
  ] : [
46306
- join7(home2, ".config/Cursor/User/workspaceStorage")
46490
+ join8(home2, ".config/Cursor/User/workspaceStorage")
46307
46491
  ]
46308
- ].filter((p) => existsSync5(p) && statSync2(p).isDirectory());
46492
+ ].filter((p) => existsSync6(p) && statSync2(p).isDirectory());
46309
46493
  setPhase("watching", `Watching ${dirs.length} directories`);
46310
46494
  const watcher = chokidar.watch(dirs, {
46311
46495
  persistent: true,
@@ -46350,8 +46534,9 @@ var init_daemon = __esm({
46350
46534
  init_src2();
46351
46535
  init_api();
46352
46536
  init_config2();
46537
+ init_lock();
46353
46538
  init_scan();
46354
- AGENT_VERSION2 = "agent-dev-0.0.19";
46539
+ AGENT_VERSION2 = "agent-dev-0.0.21";
46355
46540
  HEARTBEAT_INTERVAL_MS = 1e4;
46356
46541
  SCAN_INTERVAL_MS = 5 * 60 * 1e3;
46357
46542
  status = {
@@ -46371,37 +46556,37 @@ var watch_exports = {};
46371
46556
  __export(watch_exports, {
46372
46557
  watchForever: () => watchForever
46373
46558
  });
46374
- import { existsSync as existsSync6 } from "fs";
46375
- import { homedir as homedir5, platform as platform3 } from "os";
46376
- 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";
46377
46562
  function resolveWatchDirs() {
46378
- const home2 = homedir5();
46379
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join6(home2, ".config");
46380
- 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");
46381
46566
  const candidates = [
46382
46567
  // universal (default HOME-rooted CLI data dirs)
46383
- join6(home2, ".claude/projects"),
46384
- join6(home2, ".codex/sessions"),
46385
- join6(home2, ".cursor/ai-tracking"),
46386
- join6(home2, ".gemini"),
46387
- 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"),
46388
46573
  // XDG / Linux
46389
- join6(xdgConfig, "claude/projects"),
46390
- join6(xdgConfig, "codex/sessions"),
46391
- join6(xdgConfig, "Cursor/User/workspaceStorage"),
46392
- join6(xdgConfig, "Code/User/workspaceStorage"),
46393
- join6(xdgConfig, "Code - Insiders/User/workspaceStorage"),
46394
- 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"),
46395
46580
  // macOS
46396
46581
  ...platform3() === "darwin" ? [
46397
- join6(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46398
- join6(home2, "Library/Application Support/Claude"),
46399
- join6(home2, "Library/Application Support/Code/User/workspaceStorage"),
46400
- join6(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
46401
- 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")
46402
46587
  ] : []
46403
46588
  ];
46404
- return Array.from(new Set(candidates)).filter((p) => existsSync6(p));
46589
+ return Array.from(new Set(candidates)).filter((p) => existsSync7(p));
46405
46590
  }
46406
46591
  async function safeScan(reason) {
46407
46592
  if (scanning) {
@@ -47017,13 +47202,14 @@ async function cmdWatch() {
47017
47202
  const { watchForever: watchForever2 } = await Promise.resolve().then(() => (init_watch(), watch_exports));
47018
47203
  await watchForever2();
47019
47204
  }
47020
- async function cmdStart() {
47205
+ async function cmdStart(rest) {
47021
47206
  if (!state.bearer || !state.deviceId) {
47022
47207
  console.error("not paired yet. Run `modelstat connect` first.");
47023
47208
  process.exit(1);
47024
47209
  }
47210
+ const force = rest.includes("--force") || rest.includes("-f");
47025
47211
  const { runDaemon: runDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
47026
- await runDaemon2();
47212
+ await runDaemon2({ force });
47027
47213
  }
47028
47214
  async function cmdStop() {
47029
47215
  try {
@@ -47187,7 +47373,7 @@ async function main() {
47187
47373
  case "start":
47188
47374
  if (!state.bearer || !state.deviceId)
47189
47375
  return cmdConnect(parseConnectOpts(rest));
47190
- return cmdStart();
47376
+ return cmdStart(rest);
47191
47377
  case "connect":
47192
47378
  return cmdConnect(parseConnectOpts(rest));
47193
47379
  case "self-register":