modelstat 0.0.25 → 0.0.27

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
@@ -21131,8 +21131,8 @@ var require_snapshot_utils = __commonJS({
21131
21131
  var require_snapshot_recorder = __commonJS({
21132
21132
  "../../node_modules/.pnpm/undici@7.25.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
21133
21133
  "use strict";
21134
- var { writeFile, readFile, mkdir } = __require("fs/promises");
21135
- var { dirname: dirname7, resolve: resolve6 } = __require("path");
21134
+ var { writeFile, readFile, mkdir: mkdir2 } = __require("fs/promises");
21135
+ var { dirname: dirname9, resolve: resolve6 } = __require("path");
21136
21136
  var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("timers");
21137
21137
  var { InvalidArgumentError, UndiciError } = require_errors();
21138
21138
  var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -21363,7 +21363,7 @@ var require_snapshot_recorder = __commonJS({
21363
21363
  throw new InvalidArgumentError("Snapshot path is required");
21364
21364
  }
21365
21365
  const resolvedPath = resolve6(path5);
21366
- await mkdir(dirname7(resolvedPath), { recursive: true });
21366
+ await mkdir2(dirname9(resolvedPath), { recursive: true });
21367
21367
  const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
21368
21368
  hash,
21369
21369
  snapshot
@@ -44240,14 +44240,15 @@ var init_queue = __esm({
44240
44240
  });
44241
44241
 
44242
44242
  // ../../packages/companion-core/src/pipeline/prompts.ts
44243
- var OLLAMA_CHAT_MODEL, OLLAMA_EMBED_MODEL, SUMMARISER_SYSTEM_PROMPT, SUMMARISER_MAX_TOKENS, SUMMARISER_TEMPERATURE, QWEN_CHARS_PER_TOKEN;
44243
+ var OLLAMA_CHAT_MODEL, OLLAMA_EMBED_MODEL, SUMMARISER_SYSTEM_PROMPT, SUMMARISER_MAX_TOKENS, ABSTRACT_OUTPUT_MAX_CHARS, SUMMARISER_TEMPERATURE, QWEN_CHARS_PER_TOKEN;
44244
44244
  var init_prompts = __esm({
44245
44245
  "../../packages/companion-core/src/pipeline/prompts.ts"() {
44246
44246
  "use strict";
44247
44247
  OLLAMA_CHAT_MODEL = "qwen3.5:0.8b";
44248
44248
  OLLAMA_EMBED_MODEL = "bge-small-en-v1.5";
44249
- SUMMARISER_SYSTEM_PROMPT = "You summarise an AI coding session in ONE sentence, \u2264 240 characters. If the user message includes sampled conversation excerpts, base your summary on what the developer was actually working on (the substance \u2014 what was being built, debugged, refactored, or designed). If only metadata is given, paraphrase the metadata. Never quote the excerpts verbatim. No PII, no code literals, no file paths, no API keys. Reply with only the sentence.";
44250
- SUMMARISER_MAX_TOKENS = 120;
44249
+ SUMMARISER_SYSTEM_PROMPT = "Write the SHORTEST paragraph (1-3 sentences) that captures EXACTLY what was ACHIEVED in this coding segment, packed with the concrete domain keywords the developer used. Lead with an outcome verb \u2014 shipped, fixed, migrated, ramped, wired, diagnosed, refactored, designed, deployed, reverted, instrumented. Name the specific things touched: feature names, frameworks, components, bug classes, decisions. Density beats length: a 50-char sentence that names the actual feature beats 200 chars of filler. Skip narration ('the developer'), skip vague verbs ('worked on', 'explored', 'looked into'), skip preamble. If only metadata is given, name the project + tool + visible action concisely. Never quote excerpts verbatim. No PII, no API keys, no file paths, no code literals. Reply with ONLY the paragraph.";
44250
+ SUMMARISER_MAX_TOKENS = 160;
44251
+ ABSTRACT_OUTPUT_MAX_CHARS = 400;
44251
44252
  SUMMARISER_TEMPERATURE = 0.2;
44252
44253
  QWEN_CHARS_PER_TOKEN = 3.3;
44253
44254
  }
@@ -44287,6 +44288,7 @@ async function buildForOneSession(sessionId, events, adapters2) {
44287
44288
  const boundaries = [];
44288
44289
  let runStart = 0;
44289
44290
  let runStartMs = Date.parse(sorted[0].ts);
44291
+ let runChars = (sorted[0].content_excerpt ?? "").length;
44290
44292
  for (let i = 1; i < sorted.length; i++) {
44291
44293
  const prev2 = sorted[i - 1];
44292
44294
  const cur = sorted[i];
@@ -44297,6 +44299,7 @@ async function buildForOneSession(sessionId, events, adapters2) {
44297
44299
  if (gap >= SEGMENT_TIME_GAP_MS) split = true;
44298
44300
  else if (turnsInRun >= SEGMENT_MAX_TURNS) split = true;
44299
44301
  else if (runMs >= SEGMENT_MAX_DURATION_MS) split = true;
44302
+ else if (runChars >= SEGMENT_MAX_CONTENT_CHARS) split = true;
44300
44303
  else if (turnEmbeddings[i - 1].length > 0 && turnEmbeddings[i].length > 0 && cosineDistance(turnEmbeddings[i - 1], turnEmbeddings[i]) > SEGMENT_TOPIC_THRESHOLD) {
44301
44304
  split = true;
44302
44305
  }
@@ -44304,6 +44307,9 @@ async function buildForOneSession(sessionId, events, adapters2) {
44304
44307
  boundaries.push(i);
44305
44308
  runStart = i;
44306
44309
  runStartMs = Date.parse(cur.ts);
44310
+ runChars = (cur.content_excerpt ?? "").length;
44311
+ } else {
44312
+ runChars += (cur.content_excerpt ?? "").length;
44307
44313
  }
44308
44314
  }
44309
44315
  boundaries.push(sorted.length);
@@ -44364,13 +44370,16 @@ async function summariseSlice(sessionId, slice, adapters2) {
44364
44370
  Sampled excerpts from the conversation (already redacted of PII and secrets):
44365
44371
  ${excerptBlock}
44366
44372
 
44367
- Write ONE sentence (\u2264240 chars) describing what the human was working on. Focus on the substance \u2014 what was being built, debugged, or designed. No quotes, no PII, no code literals, no file paths.` : `Session context: ${promptFacts || "generic coding session"}.
44368
- Write one sentence describing what the human was doing.`;
44369
- let rawAbstract;
44370
- try {
44371
- rawAbstract = await adapters2.summarize({ prompt, maxTokens: 80 });
44372
- } catch {
44373
- rawAbstract = promptFacts || `${first.tool} session with ${slice.length} turns`;
44373
+ Write the SHORTEST keyword-dense paragraph (1-3 sentences, \u2264${ABSTRACT_OUTPUT_MAX_CHARS} chars) naming exactly what was achieved. Lead with an outcome verb. Pack with concrete domain keywords (frameworks, features, components, decisions). Skip narration and filler.` : `Session context: ${promptFacts || "generic coding session"}.
44374
+ Write the shortest possible paragraph naming the project + tool + visible action. Skip filler.`;
44375
+ const rawAbstract = await adapters2.summarize({
44376
+ prompt,
44377
+ maxTokens: SUMMARISER_MAX_TOKENS
44378
+ });
44379
+ if (!rawAbstract || rawAbstract.trim().length === 0) {
44380
+ throw new Error(
44381
+ `summariser returned empty abstract for session ${sessionId} (${slice.length} turns) \u2014 check that the configured summariser is healthy`
44382
+ );
44374
44383
  }
44375
44384
  const regexPass = redact(rawAbstract);
44376
44385
  let abstractText = regexPass.text;
@@ -44423,7 +44432,11 @@ Write one sentence describing what the human was doing.`;
44423
44432
  tool: first.tool,
44424
44433
  started_at: first.ts,
44425
44434
  ended_at: last.ts,
44426
- abstract: redacted.text.slice(0, ABSTRACT_MAX_CHARS),
44435
+ // Slice to the user-visible cap (ABSTRACT_OUTPUT_MAX_CHARS, default
44436
+ // 400) — well below the 512 storage cap. Models occasionally
44437
+ // overshoot the prompt's "≤N chars" instruction; the slice is the
44438
+ // hard guarantee the dashboard relies on.
44439
+ abstract: redacted.text.slice(0, ABSTRACT_OUTPUT_MAX_CHARS),
44427
44440
  tokens,
44428
44441
  tags,
44429
44442
  // counts is `Record<string, number>` after the optional model
@@ -44487,18 +44500,20 @@ function inferEnvironment(branch) {
44487
44500
  if (b === "dev" || b === "develop" || b.startsWith("dev/")) return "Dev";
44488
44501
  return null;
44489
44502
  }
44490
- var SEGMENT_TIME_GAP_MS, SEGMENT_TOPIC_THRESHOLD, SEGMENT_MAX_TURNS, SEGMENT_MAX_DURATION_MS, ABSTRACT_MAX_CHARS;
44503
+ var SEGMENT_TIME_GAP_MS, SEGMENT_TOPIC_THRESHOLD, SEGMENT_MAX_TURNS, SEGMENT_MAX_DURATION_MS, SEGMENT_MAX_CONTENT_CHARS, ABSTRACT_MAX_CHARS;
44491
44504
  var init_pipeline = __esm({
44492
44505
  "../../packages/companion-core/src/pipeline/index.ts"() {
44493
44506
  "use strict";
44494
44507
  init_redact();
44495
44508
  init_ids();
44509
+ init_prompts();
44496
44510
  init_redact();
44497
44511
  init_prompts();
44498
44512
  SEGMENT_TIME_GAP_MS = 15 * 6e4;
44499
44513
  SEGMENT_TOPIC_THRESHOLD = 0.35;
44500
44514
  SEGMENT_MAX_TURNS = 100;
44501
44515
  SEGMENT_MAX_DURATION_MS = 30 * 6e4;
44516
+ SEGMENT_MAX_CONTENT_CHARS = 12e3;
44502
44517
  ABSTRACT_MAX_CHARS = 512;
44503
44518
  }
44504
44519
  });
@@ -44520,11 +44535,123 @@ var init_src3 = __esm({
44520
44535
  // ../../packages/companion-core/src/node/file-queue-store.ts
44521
44536
  import { promises as fs3 } from "fs";
44522
44537
  import { dirname as dirname3 } from "path";
44523
- var SENT_TTL_MS;
44538
+ var SENT_TTL_MS, FileQueueStore;
44524
44539
  var init_file_queue_store = __esm({
44525
44540
  "../../packages/companion-core/src/node/file-queue-store.ts"() {
44526
44541
  "use strict";
44527
44542
  SENT_TTL_MS = 24 * 60 * 60 * 1e3;
44543
+ FileQueueStore = class {
44544
+ filePath;
44545
+ items = /* @__PURE__ */ new Map();
44546
+ loaded = false;
44547
+ writing = null;
44548
+ constructor(filePath) {
44549
+ this.filePath = filePath;
44550
+ }
44551
+ /** Lazily load the snapshot on first access so construction stays
44552
+ * synchronous (matches the old SqliteQueueStore contract). */
44553
+ async ensureLoaded() {
44554
+ if (this.loaded) return;
44555
+ try {
44556
+ const raw = await fs3.readFile(this.filePath, "utf8");
44557
+ const parsed = JSON.parse(raw);
44558
+ if (parsed.version === 1 && parsed.items) {
44559
+ const now = Date.now();
44560
+ for (const [k, v] of Object.entries(parsed.items)) {
44561
+ if (v.synced && now - v.last_event_ts_ms > SENT_TTL_MS) continue;
44562
+ this.items.set(k, v);
44563
+ }
44564
+ }
44565
+ } catch (err) {
44566
+ if (err.code !== "ENOENT") {
44567
+ try {
44568
+ const bak = await fs3.readFile(`${this.filePath}.bak`, "utf8");
44569
+ const parsed = JSON.parse(bak);
44570
+ if (parsed.version === 1 && parsed.items) {
44571
+ for (const [k, v] of Object.entries(parsed.items)) {
44572
+ this.items.set(k, v);
44573
+ }
44574
+ }
44575
+ } catch {
44576
+ }
44577
+ }
44578
+ }
44579
+ this.loaded = true;
44580
+ }
44581
+ /** Serialize writes so overlapping mutations don't race on the
44582
+ * temp-file / rename swap. writing is a single-slot promise; each
44583
+ * caller awaits the previous write and then enqueues its own. */
44584
+ async persist() {
44585
+ const prior = this.writing;
44586
+ const run = async () => {
44587
+ await prior?.catch(() => void 0);
44588
+ const snap = {
44589
+ version: 1,
44590
+ items: Object.fromEntries(this.items.entries())
44591
+ };
44592
+ const tmp = `${this.filePath}.tmp`;
44593
+ await fs3.mkdir(dirname3(this.filePath), { recursive: true });
44594
+ await fs3.writeFile(tmp, JSON.stringify(snap), "utf8");
44595
+ try {
44596
+ await fs3.rename(this.filePath, `${this.filePath}.bak`);
44597
+ } catch (err) {
44598
+ if (err.code !== "ENOENT") throw err;
44599
+ }
44600
+ await fs3.rename(tmp, this.filePath);
44601
+ };
44602
+ this.writing = run();
44603
+ try {
44604
+ await this.writing;
44605
+ } finally {
44606
+ this.writing = null;
44607
+ }
44608
+ }
44609
+ async put(item) {
44610
+ await this.ensureLoaded();
44611
+ if (this.items.has(item.source_event_id)) return;
44612
+ this.items.set(item.source_event_id, item);
44613
+ await this.persist();
44614
+ }
44615
+ async has(sourceEventId2) {
44616
+ await this.ensureLoaded();
44617
+ return this.items.has(sourceEventId2);
44618
+ }
44619
+ async listUnsentBySession() {
44620
+ await this.ensureLoaded();
44621
+ const out = /* @__PURE__ */ new Map();
44622
+ const sorted = [...this.items.values()].filter((i) => !i.synced).sort((a, b) => {
44623
+ if (a.session_id !== b.session_id)
44624
+ return a.session_id < b.session_id ? -1 : 1;
44625
+ return a.last_event_ts_ms - b.last_event_ts_ms;
44626
+ });
44627
+ for (const it of sorted) {
44628
+ const arr = out.get(it.session_id) ?? [];
44629
+ arr.push(it);
44630
+ out.set(it.session_id, arr);
44631
+ }
44632
+ return out;
44633
+ }
44634
+ async markSent(sourceEventIds, batchId2) {
44635
+ if (sourceEventIds.length === 0) return;
44636
+ await this.ensureLoaded();
44637
+ for (const id of sourceEventIds) {
44638
+ const it = this.items.get(id);
44639
+ if (!it) continue;
44640
+ it.synced = true;
44641
+ it.sent_batch_id = batchId2;
44642
+ }
44643
+ await this.persist();
44644
+ }
44645
+ async countUnsent() {
44646
+ await this.ensureLoaded();
44647
+ let n = 0;
44648
+ for (const it of this.items.values()) if (!it.synced) n++;
44649
+ return n;
44650
+ }
44651
+ /** Compat stub — Sqlite store exposed close(); nothing to release here. */
44652
+ close() {
44653
+ }
44654
+ };
44528
44655
  }
44529
44656
  });
44530
44657
 
@@ -44590,17 +44717,192 @@ var init_ollama = __esm({
44590
44717
  }
44591
44718
  });
44592
44719
 
44720
+ // ../../packages/companion-core/src/node/llama.ts
44721
+ import { mkdir } from "fs/promises";
44722
+ import { existsSync as existsSync5 } from "fs";
44723
+ import { homedir as homedir4 } from "os";
44724
+ import { dirname as dirname4, join as join3 } from "path";
44725
+ function defaultLlamaConfig() {
44726
+ const env2 = globalThis.process?.env ?? {};
44727
+ const modelsDir = env2.MODELSTAT_MODELS_DIR ?? join3(homedir4(), ".modelstat", "models");
44728
+ const modelUrl = env2.MODELSTAT_LLAMA_MODEL_URL ?? DEFAULT_LLAMA_MODEL_URL;
44729
+ const modelPath = env2.MODELSTAT_LLAMA_MODEL_PATH ?? join3(modelsDir, basenameFromUrl(modelUrl));
44730
+ return {
44731
+ modelPath,
44732
+ modelUrl,
44733
+ modelsDir,
44734
+ // Qwen3.5-4B has a 128K native context; we only need enough to
44735
+ // fit our prompt (~2 KB) plus thinking budget plus answer.
44736
+ // 4096 is plenty and keeps memory footprint reasonable on
44737
+ // older machines.
44738
+ contextSize: Number(env2.MODELSTAT_LLAMA_CONTEXT ?? 4096)
44739
+ };
44740
+ }
44741
+ function stripThinking(text) {
44742
+ return text.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/<think>[\s\S]*$/i, "").trim();
44743
+ }
44744
+ function basenameFromUrl(url) {
44745
+ const clean = url.split("?")[0].split("#")[0];
44746
+ const parts = clean.split("/");
44747
+ return parts[parts.length - 1] || "model.gguf";
44748
+ }
44749
+ async function ensureLlamaModel(cfg = defaultLlamaConfig()) {
44750
+ if (existsSync5(cfg.modelPath)) return cfg.modelPath;
44751
+ await mkdir(dirname4(cfg.modelPath), { recursive: true });
44752
+ const res = await fetch(cfg.modelUrl);
44753
+ if (!res.ok || !res.body) {
44754
+ throw new Error(
44755
+ `model download failed: ${res.status} ${res.statusText} (${cfg.modelUrl})`
44756
+ );
44757
+ }
44758
+ const total = Number(res.headers.get("content-length") ?? 0);
44759
+ const totalMb = total > 0 ? (total / (1024 * 1024)).toFixed(0) : "?";
44760
+ console.log(
44761
+ `[modelstat] downloading summariser model (~${totalMb} MB) \u2192 ${cfg.modelPath}`
44762
+ );
44763
+ const tmp = `${cfg.modelPath}.partial`;
44764
+ const { createWriteStream: createWriteStream2 } = await import("fs");
44765
+ const { Readable: Readable3 } = await import("stream");
44766
+ const { rename } = await import("fs/promises");
44767
+ const out = createWriteStream2(tmp);
44768
+ const isTty = Boolean(
44769
+ process.stdout.isTTY
44770
+ );
44771
+ let received = 0;
44772
+ let lastLog = 0;
44773
+ let lastBytes = 0;
44774
+ let lastTimeForRate = Date.now();
44775
+ const startTime = Date.now();
44776
+ const renderProgress = (final = false) => {
44777
+ const now = Date.now();
44778
+ const dt = (now - lastTimeForRate) / 1e3;
44779
+ const dBytes = received - lastBytes;
44780
+ const rateMbps = dt > 0 ? dBytes / 1024 / 1024 / dt : 0;
44781
+ lastBytes = received;
44782
+ lastTimeForRate = now;
44783
+ const mb = (received / (1024 * 1024)).toFixed(1);
44784
+ const pct = total > 0 ? (received / total * 100).toFixed(1) : "?";
44785
+ const eta = total > 0 && rateMbps > 0 ? Math.max(0, Math.round((total - received) / 1024 / 1024 / rateMbps)) : null;
44786
+ const etaStr = eta != null ? ` \xB7 ETA ${eta}s` : "";
44787
+ const elapsed = ((now - startTime) / 1e3).toFixed(0);
44788
+ const line = `[modelstat] ${mb} / ${totalMb} MB (${pct}%) \xB7 ${rateMbps.toFixed(1)} MB/s${etaStr} \xB7 ${elapsed}s`;
44789
+ if (isTty && !final) {
44790
+ process.stdout.write(`\r${line}\x1B[K`);
44791
+ } else {
44792
+ console.log(line);
44793
+ }
44794
+ };
44795
+ const nodeStream = Readable3.fromWeb(
44796
+ res.body
44797
+ );
44798
+ nodeStream.on("data", (chunk) => {
44799
+ received += chunk.length;
44800
+ const now = Date.now();
44801
+ if (now - lastLog > (isTty ? 200 : 2e3)) {
44802
+ renderProgress();
44803
+ lastLog = now;
44804
+ }
44805
+ });
44806
+ await new Promise((resolve6, reject) => {
44807
+ nodeStream.pipe(out);
44808
+ out.on("finish", () => resolve6());
44809
+ out.on("error", reject);
44810
+ nodeStream.on("error", reject);
44811
+ });
44812
+ renderProgress(true);
44813
+ if (isTty) process.stdout.write("\n");
44814
+ await rename(tmp, cfg.modelPath);
44815
+ console.log(`[modelstat] summariser model ready (${cfg.modelPath})`);
44816
+ return cfg.modelPath;
44817
+ }
44818
+ async function loadOnce(cfg) {
44819
+ if (loaded) return loaded;
44820
+ if (loadPromise) return loadPromise;
44821
+ loadPromise = (async () => {
44822
+ const llamaMod = await import("node-llama-cpp");
44823
+ const modelPath = await ensureLlamaModel(cfg);
44824
+ const llama = await llamaMod.getLlama();
44825
+ const model = await llama.loadModel({ modelPath });
44826
+ const context = await model.createContext({ contextSize: cfg.contextSize });
44827
+ const session = new llamaMod.LlamaChatSession({
44828
+ contextSequence: context.getSequence(),
44829
+ systemPrompt: SUMMARISER_SYSTEM_PROMPT
44830
+ });
44831
+ loaded = { session };
44832
+ return loaded;
44833
+ })();
44834
+ try {
44835
+ return await loadPromise;
44836
+ } catch (err) {
44837
+ loadPromise = null;
44838
+ throw err;
44839
+ }
44840
+ }
44841
+ function llamaSummarize(cfg = defaultLlamaConfig()) {
44842
+ return async ({ prompt, maxTokens }) => {
44843
+ const { session } = await loadOnce(cfg);
44844
+ const run = inflight.then(async () => {
44845
+ session.resetChatHistory();
44846
+ void maxTokens;
44847
+ const raw = await session.prompt(prompt, {
44848
+ temperature: SUMMARISER_TEMPERATURE,
44849
+ maxTokens: LLAMA_MAX_TOKENS
44850
+ });
44851
+ const stripped = stripThinking(raw ?? "");
44852
+ if (stripped.length === 0) {
44853
+ throw new Error(
44854
+ `bundled summariser produced no answer text after stripping <think> blocks (raw length=${(raw ?? "").length}). The thinking budget may be too low or the model template is misconfigured.`
44855
+ );
44856
+ }
44857
+ return stripped.slice(0, 240);
44858
+ });
44859
+ inflight = run.catch(() => void 0);
44860
+ return run;
44861
+ };
44862
+ }
44863
+ var DEFAULT_LLAMA_MODEL_URL, LLAMA_MAX_TOKENS, loaded, loadPromise, inflight;
44864
+ var init_llama = __esm({
44865
+ "../../packages/companion-core/src/node/llama.ts"() {
44866
+ "use strict";
44867
+ init_prompts();
44868
+ DEFAULT_LLAMA_MODEL_URL = "https://huggingface.co/lmstudio-community/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf";
44869
+ LLAMA_MAX_TOKENS = 1024;
44870
+ loaded = null;
44871
+ loadPromise = null;
44872
+ inflight = Promise.resolve();
44873
+ }
44874
+ });
44875
+
44593
44876
  // ../../packages/companion-core/src/node/index.ts
44877
+ var node_exports = {};
44878
+ __export(node_exports, {
44879
+ DEFAULT_LLAMA_MODEL_URL: () => DEFAULT_LLAMA_MODEL_URL,
44880
+ FileQueueStore: () => FileQueueStore,
44881
+ SqliteQueueStore: () => FileQueueStore,
44882
+ defaultLlamaConfig: () => defaultLlamaConfig,
44883
+ defaultOllamaConfig: () => defaultOllamaConfig,
44884
+ ensureLlamaModel: () => ensureLlamaModel,
44885
+ llamaSummarize: () => llamaSummarize,
44886
+ ollamaEmbed: () => ollamaEmbed,
44887
+ ollamaSummarize: () => ollamaSummarize,
44888
+ ollamaTokenize: () => ollamaTokenize
44889
+ });
44594
44890
  var init_node2 = __esm({
44595
44891
  "../../packages/companion-core/src/node/index.ts"() {
44596
44892
  "use strict";
44597
44893
  init_file_queue_store();
44598
44894
  init_file_queue_store();
44599
44895
  init_ollama();
44896
+ init_llama();
44600
44897
  }
44601
44898
  });
44602
44899
 
44603
44900
  // src/pipeline.ts
44901
+ var pipeline_exports = {};
44902
+ __export(pipeline_exports, {
44903
+ buildSegments: () => buildSegments,
44904
+ preflightSummariser: () => preflightSummariser
44905
+ });
44604
44906
  async function probeOllama(baseUrl) {
44605
44907
  try {
44606
44908
  const ctrl = new AbortController();
@@ -44615,40 +44917,58 @@ async function probeOllama(baseUrl) {
44615
44917
  return false;
44616
44918
  }
44617
44919
  }
44618
- function fallbackAdapters() {
44920
+ function bundledAdapters() {
44619
44921
  return {
44620
44922
  embed: async () => [],
44621
- // Throw → companion-core/summariseSlice's try/catch falls back to
44622
- // the `promptFacts` string which is what we want here.
44623
- summarize: async () => {
44624
- throw new Error("ollama_unavailable");
44625
- },
44923
+ summarize: llamaSummarize(defaultLlamaConfig()),
44626
44924
  tokenize: (text) => Math.max(1, Math.ceil(text.length / 4))
44627
44925
  };
44628
44926
  }
44629
44927
  async function getAdapters() {
44630
44928
  if (adapters && probed) return adapters;
44631
- const cfg = defaultOllamaConfig();
44632
- const up = await probeOllama(cfg.baseUrl);
44929
+ const ollamaCfg = defaultOllamaConfig();
44930
+ const ollamaUp = await probeOllama(ollamaCfg.baseUrl);
44633
44931
  probed = true;
44634
- if (up) {
44635
- console.log(`[modelstat] ollama up at ${cfg.baseUrl} \u2014 using LLM pipeline`);
44932
+ if (ollamaUp) {
44933
+ console.log(
44934
+ `[modelstat] ollama up at ${ollamaCfg.baseUrl} \u2014 using ${ollamaCfg.chatModel} for summarisation`
44935
+ );
44636
44936
  adapters = {
44637
- embed: ollamaEmbed(cfg),
44638
- summarize: ollamaSummarize(cfg),
44937
+ embed: ollamaEmbed(ollamaCfg),
44938
+ summarize: ollamaSummarize(ollamaCfg),
44639
44939
  tokenize: ollamaTokenize()
44640
44940
  };
44641
- } else {
44642
- console.log(
44643
- `[modelstat] ollama not reachable at ${cfg.baseUrl} \u2014 using fallback pipeline (server does classification)`
44941
+ return adapters;
44942
+ }
44943
+ try {
44944
+ await import("node-llama-cpp");
44945
+ } catch (err) {
44946
+ throw new Error(
44947
+ `modelstat agent can't start: ollama is not reachable AND the bundled summariser (node-llama-cpp) failed to load. Either start ollama (\`ollama serve\`) with \`ollama pull ${ollamaCfg.chatModel}\`, or reinstall the agent so the native binding is rebuilt for this platform. Underlying error: ${err.message}`
44644
44948
  );
44645
- adapters = fallbackAdapters();
44646
44949
  }
44950
+ console.log(
44951
+ "[modelstat] using bundled local summariser (Qwen3.5-4B, runs on this machine)"
44952
+ );
44953
+ adapters = bundledAdapters();
44647
44954
  return adapters;
44648
44955
  }
44649
44956
  async function buildSegments(events) {
44650
44957
  return buildSegmentsForSession(events, await getAdapters());
44651
44958
  }
44959
+ async function preflightSummariser() {
44960
+ const a = await getAdapters();
44961
+ const out = await a.summarize({
44962
+ prompt: 'Session context: smoke test. Sampled excerpts:\n [turn 1] "hello world"\nWrite ONE sentence (\u2264240 chars) describing what the human was doing.',
44963
+ maxTokens: 32
44964
+ });
44965
+ if (!out || out.trim().length === 0) {
44966
+ throw new Error(
44967
+ "summariser preflight returned empty output \u2014 the configured summariser is reachable but produced no text. Check the model is loaded."
44968
+ );
44969
+ }
44970
+ return out.length > 60 ? `${out.slice(0, 57)}\u2026` : out;
44971
+ }
44652
44972
  var adapters, probed;
44653
44973
  var init_pipeline2 = __esm({
44654
44974
  "src/pipeline.ts"() {
@@ -44662,23 +44982,23 @@ var init_pipeline2 = __esm({
44662
44982
 
44663
44983
  // src/scan.ts
44664
44984
  import { readdir, stat as stat2 } from "fs/promises";
44665
- import { homedir as homedir4 } from "os";
44666
- import { join as join3 } from "path";
44985
+ import { homedir as homedir5 } from "os";
44986
+ import { join as join4 } from "path";
44667
44987
  async function scanAll(cb = {}) {
44668
44988
  const deviceId = state.deviceId;
44669
44989
  if (!deviceId) throw new Error("agent not enrolled \u2014 run `register` first");
44670
44990
  const jobs = [];
44671
44991
  try {
44672
- const base = join3(homedir4(), ".claude/projects");
44992
+ const base = join4(homedir5(), ".claude/projects");
44673
44993
  const projects = await readdir(base).catch(() => []);
44674
44994
  for (const p of projects) {
44675
- const dir = join3(base, p);
44995
+ const dir = join4(base, p);
44676
44996
  const ds = await stat2(dir).catch(() => null);
44677
44997
  if (!ds?.isDirectory()) continue;
44678
44998
  const files = await readdir(dir);
44679
44999
  for (const f of files) {
44680
45000
  if (!f.endsWith(".jsonl")) continue;
44681
- const full = join3(dir, f);
45001
+ const full = join4(dir, f);
44682
45002
  jobs.push({
44683
45003
  path: full,
44684
45004
  parse: async () => {
@@ -44692,17 +45012,17 @@ async function scanAll(cb = {}) {
44692
45012
  console.warn("claude scan skipped:", e.message);
44693
45013
  }
44694
45014
  try {
44695
- const base = join3(homedir4(), ".codex/sessions");
45015
+ const base = join4(homedir5(), ".codex/sessions");
44696
45016
  const years = await readdir(base).catch(() => []);
44697
45017
  for (const y of years) {
44698
- const months = await readdir(join3(base, y)).catch(() => []);
45018
+ const months = await readdir(join4(base, y)).catch(() => []);
44699
45019
  for (const m of months) {
44700
- const days = await readdir(join3(base, y, m)).catch(() => []);
45020
+ const days = await readdir(join4(base, y, m)).catch(() => []);
44701
45021
  for (const d of days) {
44702
- const files = await readdir(join3(base, y, m, d)).catch(() => []);
45022
+ const files = await readdir(join4(base, y, m, d)).catch(() => []);
44703
45023
  for (const f of files) {
44704
45024
  if (!f.startsWith("rollout-") || !f.endsWith(".jsonl")) continue;
44705
- const full = join3(base, y, m, d, f);
45025
+ const full = join4(base, y, m, d, f);
44706
45026
  jobs.push({
44707
45027
  path: full,
44708
45028
  parse: async () => {
@@ -44784,7 +45104,7 @@ var init_scan = __esm({
44784
45104
  // src/lock.ts
44785
45105
  import {
44786
45106
  closeSync,
44787
- existsSync as existsSync6,
45107
+ existsSync as existsSync7,
44788
45108
  mkdirSync as mkdirSync3,
44789
45109
  openSync,
44790
45110
  readFileSync as readFileSync3,
@@ -44793,8 +45113,8 @@ import {
44793
45113
  writeFileSync as writeFileSync4,
44794
45114
  writeSync
44795
45115
  } from "fs";
44796
- import { homedir as homedir6 } from "os";
44797
- import { join as join5 } from "path";
45116
+ import { homedir as homedir7 } from "os";
45117
+ import { join as join6 } from "path";
44798
45118
  function isProcessAlive(pid) {
44799
45119
  if (!pid || pid <= 0) return false;
44800
45120
  try {
@@ -44895,8 +45215,8 @@ var LOCK_DIR, LOCK_FILE;
44895
45215
  var init_lock = __esm({
44896
45216
  "src/lock.ts"() {
44897
45217
  "use strict";
44898
- LOCK_DIR = join5(homedir6(), ".modelstat");
44899
- LOCK_FILE = join5(LOCK_DIR, "daemon.lock");
45218
+ LOCK_DIR = join6(homedir7(), ".modelstat");
45219
+ LOCK_FILE = join6(LOCK_DIR, "daemon.lock");
44900
45220
  }
44901
45221
  });
44902
45222
 
@@ -45633,9 +45953,9 @@ var init_handler = __esm({
45633
45953
  if (this.fsw.closed) {
45634
45954
  return;
45635
45955
  }
45636
- const dirname7 = sysPath.dirname(file);
45956
+ const dirname9 = sysPath.dirname(file);
45637
45957
  const basename4 = sysPath.basename(file);
45638
- const parent = this.fsw._getWatchedDir(dirname7);
45958
+ const parent = this.fsw._getWatchedDir(dirname9);
45639
45959
  let prevStats = stats;
45640
45960
  if (parent.has(basename4))
45641
45961
  return;
@@ -45662,7 +45982,7 @@ var init_handler = __esm({
45662
45982
  prevStats = newStats2;
45663
45983
  }
45664
45984
  } catch (error) {
45665
- this.fsw._remove(dirname7, basename4);
45985
+ this.fsw._remove(dirname9, basename4);
45666
45986
  }
45667
45987
  } else if (parent.has(basename4)) {
45668
45988
  const at = newStats.atimeMs;
@@ -46626,7 +46946,10 @@ __export(daemon_exports, {
46626
46946
  setProgress: () => setProgress,
46627
46947
  setQueue: () => setQueue
46628
46948
  });
46629
- import { existsSync as existsSync7, statSync as statSync2 } from "fs";
46949
+ import { existsSync as existsSync8, statSync as statSync2 } from "fs";
46950
+ import { readFileSync as readFileSync4 } from "fs";
46951
+ import { dirname as dirname8, join as join9 } from "path";
46952
+ import { fileURLToPath as __agentFileURLToPath } from "url";
46630
46953
  function setPhase(phase, message) {
46631
46954
  status.phase = phase;
46632
46955
  status.message = message ?? null;
@@ -46745,24 +47068,33 @@ async function runDaemon(opts = {}) {
46745
47068
  const hb = setInterval(() => void sendHeartbeat(), HEARTBEAT_INTERVAL_MS);
46746
47069
  hb.unref();
46747
47070
  void sendHeartbeat();
47071
+ try {
47072
+ setPhase("starting", "Preflight: summariser");
47073
+ const { preflightSummariser: preflightSummariser2 } = await Promise.resolve().then(() => (init_pipeline2(), pipeline_exports));
47074
+ const sample = await preflightSummariser2();
47075
+ console.log(`[modelstat] summariser preflight ok: "${sample}"`);
47076
+ } catch (err) {
47077
+ setPhase("error", `summariser preflight failed: ${err.message}`);
47078
+ throw err;
47079
+ }
46748
47080
  await runDiscovery();
46749
47081
  await runScanCycle("startup");
46750
47082
  const chokidar = (await Promise.resolve().then(() => (init_esm2(), esm_exports))).default;
46751
- const { homedir: homedir8, platform: platform5 } = await import("os");
46752
- const { join: join9 } = await import("path");
46753
- const home2 = homedir8();
47083
+ const { homedir: homedir9, platform: platform5 } = await import("os");
47084
+ const { join: join11 } = await import("path");
47085
+ const home2 = homedir9();
46754
47086
  const dirs = [
46755
- join9(home2, ".claude/projects"),
46756
- join9(home2, ".codex/sessions"),
46757
- join9(home2, ".cursor/ai-tracking"),
46758
- join9(home2, ".gemini"),
47087
+ join11(home2, ".claude/projects"),
47088
+ join11(home2, ".codex/sessions"),
47089
+ join11(home2, ".cursor/ai-tracking"),
47090
+ join11(home2, ".gemini"),
46759
47091
  ...platform5() === "darwin" ? [
46760
- join9(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46761
- join9(home2, "Library/Application Support/Claude")
47092
+ join11(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
47093
+ join11(home2, "Library/Application Support/Claude")
46762
47094
  ] : [
46763
- join9(home2, ".config/Cursor/User/workspaceStorage")
47095
+ join11(home2, ".config/Cursor/User/workspaceStorage")
46764
47096
  ]
46765
- ].filter((p) => existsSync7(p) && statSync2(p).isDirectory());
47097
+ ].filter((p) => existsSync8(p) && statSync2(p).isDirectory());
46766
47098
  setPhase("watching", `Watching ${dirs.length} directories`);
46767
47099
  const watcher = chokidar.watch(dirs, {
46768
47100
  persistent: true,
@@ -46809,7 +47141,24 @@ var init_daemon = __esm({
46809
47141
  init_config2();
46810
47142
  init_lock();
46811
47143
  init_scan();
46812
- AGENT_VERSION2 = "agent-dev-0.0.23";
47144
+ AGENT_VERSION2 = (() => {
47145
+ try {
47146
+ const here2 = dirname8(__agentFileURLToPath(import.meta.url));
47147
+ const candidates = [
47148
+ join9(here2, "..", "package.json"),
47149
+ join9(here2, "..", "..", "package.json")
47150
+ ];
47151
+ for (const c of candidates) {
47152
+ try {
47153
+ const v = JSON.parse(readFileSync4(c, "utf8")).version;
47154
+ if (typeof v === "string") return `agent-${v}`;
47155
+ } catch {
47156
+ }
47157
+ }
47158
+ } catch {
47159
+ }
47160
+ return "agent-unknown";
47161
+ })();
46813
47162
  HEARTBEAT_INTERVAL_MS = 1e4;
46814
47163
  SCAN_INTERVAL_MS = 5 * 60 * 1e3;
46815
47164
  status = {
@@ -46829,37 +47178,37 @@ var watch_exports = {};
46829
47178
  __export(watch_exports, {
46830
47179
  watchForever: () => watchForever
46831
47180
  });
46832
- import { existsSync as existsSync8 } from "fs";
46833
- import { homedir as homedir7, platform as platform3 } from "os";
46834
- import { join as join8 } from "path";
47181
+ import { existsSync as existsSync9 } from "fs";
47182
+ import { homedir as homedir8, platform as platform3 } from "os";
47183
+ import { join as join10 } from "path";
46835
47184
  function resolveWatchDirs() {
46836
- const home2 = homedir7();
46837
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join8(home2, ".config");
46838
- const xdgData = process.env.XDG_DATA_HOME ?? join8(home2, ".local/share");
47185
+ const home2 = homedir8();
47186
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join10(home2, ".config");
47187
+ const xdgData = process.env.XDG_DATA_HOME ?? join10(home2, ".local/share");
46839
47188
  const candidates = [
46840
47189
  // universal (default HOME-rooted CLI data dirs)
46841
- join8(home2, ".claude/projects"),
46842
- join8(home2, ".codex/sessions"),
46843
- join8(home2, ".cursor/ai-tracking"),
46844
- join8(home2, ".gemini"),
46845
- join8(home2, ".aider"),
47190
+ join10(home2, ".claude/projects"),
47191
+ join10(home2, ".codex/sessions"),
47192
+ join10(home2, ".cursor/ai-tracking"),
47193
+ join10(home2, ".gemini"),
47194
+ join10(home2, ".aider"),
46846
47195
  // XDG / Linux
46847
- join8(xdgConfig, "claude/projects"),
46848
- join8(xdgConfig, "codex/sessions"),
46849
- join8(xdgConfig, "Cursor/User/workspaceStorage"),
46850
- join8(xdgConfig, "Code/User/workspaceStorage"),
46851
- join8(xdgConfig, "Code - Insiders/User/workspaceStorage"),
46852
- join8(xdgData, "claude/projects"),
47196
+ join10(xdgConfig, "claude/projects"),
47197
+ join10(xdgConfig, "codex/sessions"),
47198
+ join10(xdgConfig, "Cursor/User/workspaceStorage"),
47199
+ join10(xdgConfig, "Code/User/workspaceStorage"),
47200
+ join10(xdgConfig, "Code - Insiders/User/workspaceStorage"),
47201
+ join10(xdgData, "claude/projects"),
46853
47202
  // macOS
46854
47203
  ...platform3() === "darwin" ? [
46855
- join8(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46856
- join8(home2, "Library/Application Support/Claude"),
46857
- join8(home2, "Library/Application Support/Code/User/workspaceStorage"),
46858
- join8(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
46859
- join8(home2, "Library/Application Support/Zed")
47204
+ join10(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
47205
+ join10(home2, "Library/Application Support/Claude"),
47206
+ join10(home2, "Library/Application Support/Code/User/workspaceStorage"),
47207
+ join10(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
47208
+ join10(home2, "Library/Application Support/Zed")
46860
47209
  ] : []
46861
47210
  ];
46862
- return Array.from(new Set(candidates)).filter((p) => existsSync8(p));
47211
+ return Array.from(new Set(candidates)).filter((p) => existsSync9(p));
46863
47212
  }
46864
47213
  async function safeScan(reason) {
46865
47214
  if (scanning) {
@@ -46932,30 +47281,30 @@ import { createInterface as createInterface3 } from "readline";
46932
47281
  import { spawnSync } from "child_process";
46933
47282
  import {
46934
47283
  copyFileSync,
46935
- existsSync as existsSync5,
47284
+ existsSync as existsSync6,
46936
47285
  mkdirSync as mkdirSync2,
46937
47286
  unlinkSync,
46938
47287
  writeFileSync as writeFileSync3
46939
47288
  } from "fs";
46940
- import { homedir as homedir5, platform as platform2, userInfo } from "os";
46941
- import { dirname as dirname4, join as join4 } from "path";
47289
+ import { homedir as homedir6, platform as platform2, userInfo } from "os";
47290
+ import { dirname as dirname5, join as join5 } from "path";
46942
47291
  import { fileURLToPath as fileURLToPath2 } from "url";
46943
47292
  var SERVICE_LABEL = "ai.modelstat.agent";
46944
47293
  var SYSTEMD_UNIT = "modelstat";
46945
47294
  function home() {
46946
- return homedir5();
47295
+ return homedir6();
46947
47296
  }
46948
47297
  function stateDir() {
46949
- return join4(home(), ".modelstat");
47298
+ return join5(home(), ".modelstat");
46950
47299
  }
46951
47300
  function binDir() {
46952
- return join4(stateDir(), "bin");
47301
+ return join5(stateDir(), "bin");
46953
47302
  }
46954
47303
  function logDir() {
46955
- return join4(stateDir(), "logs");
47304
+ return join5(stateDir(), "logs");
46956
47305
  }
46957
47306
  function installedCliPath() {
46958
- return join4(binDir(), "modelstat.mjs");
47307
+ return join5(binDir(), "modelstat.mjs");
46959
47308
  }
46960
47309
  function runningCliPath() {
46961
47310
  return fileURLToPath2(import.meta.url).replace(/service\.(mjs|js|ts)$/, "cli.mjs");
@@ -46965,7 +47314,7 @@ function installBundle() {
46965
47314
  mkdirSync2(logDir(), { recursive: true });
46966
47315
  const src = runningCliPath();
46967
47316
  const dest = installedCliPath();
46968
- if (!existsSync5(src)) {
47317
+ if (!existsSync6(src)) {
46969
47318
  throw new Error(
46970
47319
  `Can't find the CLI bundle to install from (${src}). Are you running a local dev build?`
46971
47320
  );
@@ -46977,21 +47326,21 @@ function nodeBinary() {
46977
47326
  return process.execPath;
46978
47327
  }
46979
47328
  function plistPath() {
46980
- return join4(home(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
47329
+ return join5(home(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
46981
47330
  }
46982
47331
  function locateTrayExecutable() {
46983
47332
  const candidates = [
46984
- join4(home(), "Applications", "ModelstatTray.app", "Contents", "MacOS", "modelstat-tray"),
47333
+ join5(home(), "Applications", "ModelstatTray.app", "Contents", "MacOS", "modelstat-tray"),
46985
47334
  "/Applications/ModelstatTray.app/Contents/MacOS/modelstat-tray"
46986
47335
  ];
46987
47336
  for (const p of candidates) {
46988
- if (existsSync5(p)) return p;
47337
+ if (existsSync6(p)) return p;
46989
47338
  }
46990
47339
  return null;
46991
47340
  }
46992
47341
  function writePlist(cliPath) {
46993
47342
  const p = plistPath();
46994
- mkdirSync2(dirname4(p), { recursive: true });
47343
+ mkdirSync2(dirname5(p), { recursive: true });
46995
47344
  const tray = locateTrayExecutable();
46996
47345
  const programArgs = tray ? ` <string>${tray}</string>` : [
46997
47346
  ` <string>${nodeBinary()}</string>`,
@@ -47011,8 +47360,8 @@ ${programArgs}
47011
47360
  <key>KeepAlive</key>
47012
47361
  <dict><key>SuccessfulExit</key><false/></dict>
47013
47362
  <key>ThrottleInterval</key><integer>30</integer>
47014
- <key>StandardOutPath</key><string>${join4(logDir(), "out.log")}</string>
47015
- <key>StandardErrorPath</key><string>${join4(logDir(), "err.log")}</string>
47363
+ <key>StandardOutPath</key><string>${join5(logDir(), "out.log")}</string>
47364
+ <key>StandardErrorPath</key><string>${join5(logDir(), "err.log")}</string>
47016
47365
  <key>EnvironmentVariables</key>
47017
47366
  <dict>
47018
47367
  <key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
@@ -47052,7 +47401,7 @@ function macUninstall() {
47052
47401
  const target = `gui/${uid}/${SERVICE_LABEL}`;
47053
47402
  launchctl(["bootout", target]);
47054
47403
  const plist = plistPath();
47055
- if (existsSync5(plist)) {
47404
+ if (existsSync6(plist)) {
47056
47405
  try {
47057
47406
  unlinkSync(plist);
47058
47407
  } catch {
@@ -47065,12 +47414,12 @@ function macStatus() {
47065
47414
  return { running: r.ok, hint: r.ok ? "launchd managed" : "not installed" };
47066
47415
  }
47067
47416
  function systemdUnitPath() {
47068
- const xdg = process.env.XDG_CONFIG_HOME ?? join4(home(), ".config");
47069
- return join4(xdg, "systemd", "user", `${SYSTEMD_UNIT}.service`);
47417
+ const xdg = process.env.XDG_CONFIG_HOME ?? join5(home(), ".config");
47418
+ return join5(xdg, "systemd", "user", `${SYSTEMD_UNIT}.service`);
47070
47419
  }
47071
47420
  function writeSystemdUnit(cliPath) {
47072
47421
  const unitPath = systemdUnitPath();
47073
- mkdirSync2(dirname4(unitPath), { recursive: true });
47422
+ mkdirSync2(dirname5(unitPath), { recursive: true });
47074
47423
  const unit = `[Unit]
47075
47424
  Description=modelstat agent
47076
47425
  Documentation=https://modelstat.ai
@@ -47085,8 +47434,8 @@ RestartSec=10
47085
47434
  # Don't restart-storm if the service is persistently unreachable.
47086
47435
  StartLimitIntervalSec=300
47087
47436
  StartLimitBurst=10
47088
- StandardOutput=append:${join4(logDir(), "out.log")}
47089
- StandardError=append:${join4(logDir(), "err.log")}
47437
+ StandardOutput=append:${join5(logDir(), "out.log")}
47438
+ StandardError=append:${join5(logDir(), "err.log")}
47090
47439
 
47091
47440
  [Install]
47092
47441
  WantedBy=default.target
@@ -47111,7 +47460,7 @@ function linuxInstall() {
47111
47460
  function linuxUninstall() {
47112
47461
  systemctl(["disable", "--now", `${SYSTEMD_UNIT}.service`]);
47113
47462
  const unit = systemdUnitPath();
47114
- if (existsSync5(unit)) {
47463
+ if (existsSync6(unit)) {
47115
47464
  try {
47116
47465
  unlinkSync(unit);
47117
47466
  } catch {
@@ -47155,9 +47504,9 @@ function logsDir() {
47155
47504
  }
47156
47505
  function installTrayApp(sourceAppPath) {
47157
47506
  if (platform2() !== "darwin") return null;
47158
- if (!existsSync5(sourceAppPath)) return null;
47159
- const dest = join4(home(), "Applications", "ModelstatTray.app");
47160
- mkdirSync2(dirname4(dest), { recursive: true });
47507
+ if (!existsSync6(sourceAppPath)) return null;
47508
+ const dest = join5(home(), "Applications", "ModelstatTray.app");
47509
+ mkdirSync2(dirname5(dest), { recursive: true });
47161
47510
  spawnSync("rm", ["-rf", dest]);
47162
47511
  const r = spawnSync("cp", ["-R", sourceAppPath, dest], { encoding: "utf8" });
47163
47512
  if (r.status !== 0) {
@@ -47167,28 +47516,28 @@ function installTrayApp(sourceAppPath) {
47167
47516
  }
47168
47517
  function bundledTrayAppPath() {
47169
47518
  if (platform2() !== "darwin") return null;
47170
- const here2 = dirname4(fileURLToPath2(import.meta.url));
47519
+ const here2 = dirname5(fileURLToPath2(import.meta.url));
47171
47520
  const candidates = [
47172
47521
  // Pre-built .app — CI with codesigning drops one here.
47173
- join4(here2, "..", "vendor", "ModelstatTray.app"),
47522
+ join5(here2, "..", "vendor", "ModelstatTray.app"),
47174
47523
  // Local dev layout: apps/agent-dev/src/service.ts → ../../tray-mac/build/ModelstatTray.app
47175
- join4(here2, "..", "..", "tray-mac", "build", "ModelstatTray.app")
47524
+ join5(here2, "..", "..", "tray-mac", "build", "ModelstatTray.app")
47176
47525
  ];
47177
47526
  for (const c of candidates) {
47178
- if (existsSync5(c)) return c;
47527
+ if (existsSync6(c)) return c;
47179
47528
  }
47180
47529
  const sourceDirs = [
47181
- join4(here2, "..", "vendor", "tray-mac"),
47182
- join4(here2, "..", "..", "tray-mac")
47530
+ join5(here2, "..", "vendor", "tray-mac"),
47531
+ join5(here2, "..", "..", "tray-mac")
47183
47532
  ];
47184
47533
  for (const src of sourceDirs) {
47185
- const build = join4(src, "build-app.sh");
47186
- if (!existsSync5(build)) continue;
47534
+ const build = join5(src, "build-app.sh");
47535
+ if (!existsSync6(build)) continue;
47187
47536
  if (!hasSwift()) return null;
47188
47537
  const r = spawnSync("bash", [build], { cwd: src, encoding: "utf8" });
47189
47538
  if (r.status === 0) {
47190
- const app = join4(src, "build", "ModelstatTray.app");
47191
- if (existsSync5(app)) return app;
47539
+ const app = join5(src, "build", "ModelstatTray.app");
47540
+ if (existsSync6(app)) return app;
47192
47541
  }
47193
47542
  }
47194
47543
  return null;
@@ -47420,12 +47769,33 @@ async function cmdConnect(opts) {
47420
47769
  warn(`tray install skipped: ${e.message}`);
47421
47770
  }
47422
47771
  }
47772
+ step("Preparing local summariser (downloads on first run)");
47773
+ let modelReady = false;
47774
+ try {
47775
+ const { ensureLlamaModel: ensureLlamaModel2, defaultLlamaConfig: defaultLlamaConfig2 } = await Promise.resolve().then(() => (init_node2(), node_exports));
47776
+ await ensureLlamaModel2(defaultLlamaConfig2());
47777
+ modelReady = true;
47778
+ emitEvent(opts, "summariser_model_ready", {});
47779
+ ok("summariser model on disk");
47780
+ } catch (e) {
47781
+ emitEvent(opts, "summariser_model_failed", {
47782
+ error: e.message
47783
+ });
47784
+ warn(`couldn't prepare summariser model: ${e.message}`);
47785
+ warn(
47786
+ "the background service will retry the download on its first scan"
47787
+ );
47788
+ }
47423
47789
  step("Installing background service so the agent survives reboots");
47424
47790
  let serviceOk = false;
47425
47791
  try {
47426
47792
  const svc = installService();
47427
47793
  serviceOk = true;
47428
- emitEvent(opts, "service_installed", { path: svc.path, logs: svc.logs });
47794
+ emitEvent(opts, "service_installed", {
47795
+ path: svc.path,
47796
+ logs: svc.logs,
47797
+ summariser_ready: modelReady
47798
+ });
47429
47799
  ok(`${platform4() === "darwin" ? "launchd" : "systemd --user"}: ${svc.path}`);
47430
47800
  } catch (e) {
47431
47801
  emitEvent(opts, "service_install_failed", { error: e.message });
@@ -47494,6 +47864,9 @@ async function cmdDiscover() {
47494
47864
  console.log("\u2713 reported to backend");
47495
47865
  }
47496
47866
  async function cmdScan() {
47867
+ const { preflightSummariser: preflightSummariser2 } = await Promise.resolve().then(() => (init_pipeline2(), pipeline_exports));
47868
+ const sample = await preflightSummariser2();
47869
+ console.log(`[modelstat] summariser preflight ok: "${sample}"`);
47497
47870
  const r = await scanAll();
47498
47871
  console.log();
47499
47872
  console.log(