modelstat 0.0.24 → 0.0.26

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
@@ -1,4 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ {
3
+ const [__msMaj, __msMin] = process.versions.node.split('.').map(Number);
4
+ if (__msMaj < 20 || (__msMaj === 20 && __msMin < 18)) {
5
+ process.stderr.write(`modelstat requires Node \u2265 20.18 (you have ${process.version}).\n`);
6
+ process.stderr.write('Install Node 20+: https://nodejs.org\n');
7
+ process.stderr.write('Debian/Ubuntu: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs\n');
8
+ process.exit(1);
9
+ }
10
+ }
2
11
  import { createRequire as __modelstatCR } from "node:module";
3
12
  const require = __modelstatCR(import.meta.url);
4
13
  var __create = Object.create;
@@ -21122,8 +21131,8 @@ var require_snapshot_utils = __commonJS({
21122
21131
  var require_snapshot_recorder = __commonJS({
21123
21132
  "../../node_modules/.pnpm/undici@7.25.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
21124
21133
  "use strict";
21125
- var { writeFile, readFile, mkdir } = __require("fs/promises");
21126
- var { dirname: dirname7, resolve: resolve6 } = __require("path");
21134
+ var { writeFile, readFile, mkdir: mkdir2 } = __require("fs/promises");
21135
+ var { dirname: dirname8, resolve: resolve6 } = __require("path");
21127
21136
  var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("timers");
21128
21137
  var { InvalidArgumentError, UndiciError } = require_errors();
21129
21138
  var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -21354,7 +21363,7 @@ var require_snapshot_recorder = __commonJS({
21354
21363
  throw new InvalidArgumentError("Snapshot path is required");
21355
21364
  }
21356
21365
  const resolvedPath = resolve6(path5);
21357
- await mkdir(dirname7(resolvedPath), { recursive: true });
21366
+ await mkdir2(dirname8(resolvedPath), { recursive: true });
21358
21367
  const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
21359
21368
  hash,
21360
21369
  snapshot
@@ -44231,14 +44240,15 @@ var init_queue = __esm({
44231
44240
  });
44232
44241
 
44233
44242
  // ../../packages/companion-core/src/pipeline/prompts.ts
44234
- 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;
44235
44244
  var init_prompts = __esm({
44236
44245
  "../../packages/companion-core/src/pipeline/prompts.ts"() {
44237
44246
  "use strict";
44238
44247
  OLLAMA_CHAT_MODEL = "qwen3.5:0.8b";
44239
44248
  OLLAMA_EMBED_MODEL = "bge-small-en-v1.5";
44240
- 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.";
44241
- 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;
44242
44252
  SUMMARISER_TEMPERATURE = 0.2;
44243
44253
  QWEN_CHARS_PER_TOKEN = 3.3;
44244
44254
  }
@@ -44278,6 +44288,7 @@ async function buildForOneSession(sessionId, events, adapters2) {
44278
44288
  const boundaries = [];
44279
44289
  let runStart = 0;
44280
44290
  let runStartMs = Date.parse(sorted[0].ts);
44291
+ let runChars = (sorted[0].content_excerpt ?? "").length;
44281
44292
  for (let i = 1; i < sorted.length; i++) {
44282
44293
  const prev2 = sorted[i - 1];
44283
44294
  const cur = sorted[i];
@@ -44288,6 +44299,7 @@ async function buildForOneSession(sessionId, events, adapters2) {
44288
44299
  if (gap >= SEGMENT_TIME_GAP_MS) split = true;
44289
44300
  else if (turnsInRun >= SEGMENT_MAX_TURNS) split = true;
44290
44301
  else if (runMs >= SEGMENT_MAX_DURATION_MS) split = true;
44302
+ else if (runChars >= SEGMENT_MAX_CONTENT_CHARS) split = true;
44291
44303
  else if (turnEmbeddings[i - 1].length > 0 && turnEmbeddings[i].length > 0 && cosineDistance(turnEmbeddings[i - 1], turnEmbeddings[i]) > SEGMENT_TOPIC_THRESHOLD) {
44292
44304
  split = true;
44293
44305
  }
@@ -44295,6 +44307,9 @@ async function buildForOneSession(sessionId, events, adapters2) {
44295
44307
  boundaries.push(i);
44296
44308
  runStart = i;
44297
44309
  runStartMs = Date.parse(cur.ts);
44310
+ runChars = (cur.content_excerpt ?? "").length;
44311
+ } else {
44312
+ runChars += (cur.content_excerpt ?? "").length;
44298
44313
  }
44299
44314
  }
44300
44315
  boundaries.push(sorted.length);
@@ -44355,13 +44370,16 @@ async function summariseSlice(sessionId, slice, adapters2) {
44355
44370
  Sampled excerpts from the conversation (already redacted of PII and secrets):
44356
44371
  ${excerptBlock}
44357
44372
 
44358
- 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"}.
44359
- Write one sentence describing what the human was doing.`;
44360
- let rawAbstract;
44361
- try {
44362
- rawAbstract = await adapters2.summarize({ prompt, maxTokens: 80 });
44363
- } catch {
44364
- 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
+ );
44365
44383
  }
44366
44384
  const regexPass = redact(rawAbstract);
44367
44385
  let abstractText = regexPass.text;
@@ -44414,7 +44432,11 @@ Write one sentence describing what the human was doing.`;
44414
44432
  tool: first.tool,
44415
44433
  started_at: first.ts,
44416
44434
  ended_at: last.ts,
44417
- 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),
44418
44440
  tokens,
44419
44441
  tags,
44420
44442
  // counts is `Record<string, number>` after the optional model
@@ -44478,18 +44500,20 @@ function inferEnvironment(branch) {
44478
44500
  if (b === "dev" || b === "develop" || b.startsWith("dev/")) return "Dev";
44479
44501
  return null;
44480
44502
  }
44481
- 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;
44482
44504
  var init_pipeline = __esm({
44483
44505
  "../../packages/companion-core/src/pipeline/index.ts"() {
44484
44506
  "use strict";
44485
44507
  init_redact();
44486
44508
  init_ids();
44509
+ init_prompts();
44487
44510
  init_redact();
44488
44511
  init_prompts();
44489
44512
  SEGMENT_TIME_GAP_MS = 15 * 6e4;
44490
44513
  SEGMENT_TOPIC_THRESHOLD = 0.35;
44491
44514
  SEGMENT_MAX_TURNS = 100;
44492
44515
  SEGMENT_MAX_DURATION_MS = 30 * 6e4;
44516
+ SEGMENT_MAX_CONTENT_CHARS = 12e3;
44493
44517
  ABSTRACT_MAX_CHARS = 512;
44494
44518
  }
44495
44519
  });
@@ -44511,11 +44535,123 @@ var init_src3 = __esm({
44511
44535
  // ../../packages/companion-core/src/node/file-queue-store.ts
44512
44536
  import { promises as fs3 } from "fs";
44513
44537
  import { dirname as dirname3 } from "path";
44514
- var SENT_TTL_MS;
44538
+ var SENT_TTL_MS, FileQueueStore;
44515
44539
  var init_file_queue_store = __esm({
44516
44540
  "../../packages/companion-core/src/node/file-queue-store.ts"() {
44517
44541
  "use strict";
44518
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
+ };
44519
44655
  }
44520
44656
  });
44521
44657
 
@@ -44581,17 +44717,192 @@ var init_ollama = __esm({
44581
44717
  }
44582
44718
  });
44583
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
+
44584
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
+ });
44585
44890
  var init_node2 = __esm({
44586
44891
  "../../packages/companion-core/src/node/index.ts"() {
44587
44892
  "use strict";
44588
44893
  init_file_queue_store();
44589
44894
  init_file_queue_store();
44590
44895
  init_ollama();
44896
+ init_llama();
44591
44897
  }
44592
44898
  });
44593
44899
 
44594
44900
  // src/pipeline.ts
44901
+ var pipeline_exports = {};
44902
+ __export(pipeline_exports, {
44903
+ buildSegments: () => buildSegments,
44904
+ preflightSummariser: () => preflightSummariser
44905
+ });
44595
44906
  async function probeOllama(baseUrl) {
44596
44907
  try {
44597
44908
  const ctrl = new AbortController();
@@ -44606,40 +44917,58 @@ async function probeOllama(baseUrl) {
44606
44917
  return false;
44607
44918
  }
44608
44919
  }
44609
- function fallbackAdapters() {
44920
+ function bundledAdapters() {
44610
44921
  return {
44611
44922
  embed: async () => [],
44612
- // Throw → companion-core/summariseSlice's try/catch falls back to
44613
- // the `promptFacts` string which is what we want here.
44614
- summarize: async () => {
44615
- throw new Error("ollama_unavailable");
44616
- },
44923
+ summarize: llamaSummarize(defaultLlamaConfig()),
44617
44924
  tokenize: (text) => Math.max(1, Math.ceil(text.length / 4))
44618
44925
  };
44619
44926
  }
44620
44927
  async function getAdapters() {
44621
44928
  if (adapters && probed) return adapters;
44622
- const cfg = defaultOllamaConfig();
44623
- const up = await probeOllama(cfg.baseUrl);
44929
+ const ollamaCfg = defaultOllamaConfig();
44930
+ const ollamaUp = await probeOllama(ollamaCfg.baseUrl);
44624
44931
  probed = true;
44625
- if (up) {
44626
- 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
+ );
44627
44936
  adapters = {
44628
- embed: ollamaEmbed(cfg),
44629
- summarize: ollamaSummarize(cfg),
44937
+ embed: ollamaEmbed(ollamaCfg),
44938
+ summarize: ollamaSummarize(ollamaCfg),
44630
44939
  tokenize: ollamaTokenize()
44631
44940
  };
44632
- } else {
44633
- console.log(
44634
- `[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}`
44635
44948
  );
44636
- adapters = fallbackAdapters();
44637
44949
  }
44950
+ console.log(
44951
+ "[modelstat] using bundled local summariser (Qwen3.5-4B, runs on this machine)"
44952
+ );
44953
+ adapters = bundledAdapters();
44638
44954
  return adapters;
44639
44955
  }
44640
44956
  async function buildSegments(events) {
44641
44957
  return buildSegmentsForSession(events, await getAdapters());
44642
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
+ }
44643
44972
  var adapters, probed;
44644
44973
  var init_pipeline2 = __esm({
44645
44974
  "src/pipeline.ts"() {
@@ -44653,23 +44982,23 @@ var init_pipeline2 = __esm({
44653
44982
 
44654
44983
  // src/scan.ts
44655
44984
  import { readdir, stat as stat2 } from "fs/promises";
44656
- import { homedir as homedir4 } from "os";
44657
- import { join as join3 } from "path";
44985
+ import { homedir as homedir5 } from "os";
44986
+ import { join as join4 } from "path";
44658
44987
  async function scanAll(cb = {}) {
44659
44988
  const deviceId = state.deviceId;
44660
44989
  if (!deviceId) throw new Error("agent not enrolled \u2014 run `register` first");
44661
44990
  const jobs = [];
44662
44991
  try {
44663
- const base = join3(homedir4(), ".claude/projects");
44992
+ const base = join4(homedir5(), ".claude/projects");
44664
44993
  const projects = await readdir(base).catch(() => []);
44665
44994
  for (const p of projects) {
44666
- const dir = join3(base, p);
44995
+ const dir = join4(base, p);
44667
44996
  const ds = await stat2(dir).catch(() => null);
44668
44997
  if (!ds?.isDirectory()) continue;
44669
44998
  const files = await readdir(dir);
44670
44999
  for (const f of files) {
44671
45000
  if (!f.endsWith(".jsonl")) continue;
44672
- const full = join3(dir, f);
45001
+ const full = join4(dir, f);
44673
45002
  jobs.push({
44674
45003
  path: full,
44675
45004
  parse: async () => {
@@ -44683,17 +45012,17 @@ async function scanAll(cb = {}) {
44683
45012
  console.warn("claude scan skipped:", e.message);
44684
45013
  }
44685
45014
  try {
44686
- const base = join3(homedir4(), ".codex/sessions");
45015
+ const base = join4(homedir5(), ".codex/sessions");
44687
45016
  const years = await readdir(base).catch(() => []);
44688
45017
  for (const y of years) {
44689
- const months = await readdir(join3(base, y)).catch(() => []);
45018
+ const months = await readdir(join4(base, y)).catch(() => []);
44690
45019
  for (const m of months) {
44691
- const days = await readdir(join3(base, y, m)).catch(() => []);
45020
+ const days = await readdir(join4(base, y, m)).catch(() => []);
44692
45021
  for (const d of days) {
44693
- const files = await readdir(join3(base, y, m, d)).catch(() => []);
45022
+ const files = await readdir(join4(base, y, m, d)).catch(() => []);
44694
45023
  for (const f of files) {
44695
45024
  if (!f.startsWith("rollout-") || !f.endsWith(".jsonl")) continue;
44696
- const full = join3(base, y, m, d, f);
45025
+ const full = join4(base, y, m, d, f);
44697
45026
  jobs.push({
44698
45027
  path: full,
44699
45028
  parse: async () => {
@@ -44775,7 +45104,7 @@ var init_scan = __esm({
44775
45104
  // src/lock.ts
44776
45105
  import {
44777
45106
  closeSync,
44778
- existsSync as existsSync6,
45107
+ existsSync as existsSync7,
44779
45108
  mkdirSync as mkdirSync3,
44780
45109
  openSync,
44781
45110
  readFileSync as readFileSync3,
@@ -44784,8 +45113,8 @@ import {
44784
45113
  writeFileSync as writeFileSync4,
44785
45114
  writeSync
44786
45115
  } from "fs";
44787
- import { homedir as homedir6 } from "os";
44788
- import { join as join5 } from "path";
45116
+ import { homedir as homedir7 } from "os";
45117
+ import { join as join6 } from "path";
44789
45118
  function isProcessAlive(pid) {
44790
45119
  if (!pid || pid <= 0) return false;
44791
45120
  try {
@@ -44886,8 +45215,8 @@ var LOCK_DIR, LOCK_FILE;
44886
45215
  var init_lock = __esm({
44887
45216
  "src/lock.ts"() {
44888
45217
  "use strict";
44889
- LOCK_DIR = join5(homedir6(), ".modelstat");
44890
- LOCK_FILE = join5(LOCK_DIR, "daemon.lock");
45218
+ LOCK_DIR = join6(homedir7(), ".modelstat");
45219
+ LOCK_FILE = join6(LOCK_DIR, "daemon.lock");
44891
45220
  }
44892
45221
  });
44893
45222
 
@@ -45624,9 +45953,9 @@ var init_handler = __esm({
45624
45953
  if (this.fsw.closed) {
45625
45954
  return;
45626
45955
  }
45627
- const dirname7 = sysPath.dirname(file);
45956
+ const dirname8 = sysPath.dirname(file);
45628
45957
  const basename4 = sysPath.basename(file);
45629
- const parent = this.fsw._getWatchedDir(dirname7);
45958
+ const parent = this.fsw._getWatchedDir(dirname8);
45630
45959
  let prevStats = stats;
45631
45960
  if (parent.has(basename4))
45632
45961
  return;
@@ -45653,7 +45982,7 @@ var init_handler = __esm({
45653
45982
  prevStats = newStats2;
45654
45983
  }
45655
45984
  } catch (error) {
45656
- this.fsw._remove(dirname7, basename4);
45985
+ this.fsw._remove(dirname8, basename4);
45657
45986
  }
45658
45987
  } else if (parent.has(basename4)) {
45659
45988
  const at = newStats.atimeMs;
@@ -46617,7 +46946,7 @@ __export(daemon_exports, {
46617
46946
  setProgress: () => setProgress,
46618
46947
  setQueue: () => setQueue
46619
46948
  });
46620
- import { existsSync as existsSync7, statSync as statSync2 } from "fs";
46949
+ import { existsSync as existsSync8, statSync as statSync2 } from "fs";
46621
46950
  function setPhase(phase, message) {
46622
46951
  status.phase = phase;
46623
46952
  status.message = message ?? null;
@@ -46736,24 +47065,33 @@ async function runDaemon(opts = {}) {
46736
47065
  const hb = setInterval(() => void sendHeartbeat(), HEARTBEAT_INTERVAL_MS);
46737
47066
  hb.unref();
46738
47067
  void sendHeartbeat();
47068
+ try {
47069
+ setPhase("starting", "Preflight: summariser");
47070
+ const { preflightSummariser: preflightSummariser2 } = await Promise.resolve().then(() => (init_pipeline2(), pipeline_exports));
47071
+ const sample = await preflightSummariser2();
47072
+ console.log(`[modelstat] summariser preflight ok: "${sample}"`);
47073
+ } catch (err) {
47074
+ setPhase("error", `summariser preflight failed: ${err.message}`);
47075
+ throw err;
47076
+ }
46739
47077
  await runDiscovery();
46740
47078
  await runScanCycle("startup");
46741
47079
  const chokidar = (await Promise.resolve().then(() => (init_esm2(), esm_exports))).default;
46742
- const { homedir: homedir8, platform: platform5 } = await import("os");
46743
- const { join: join9 } = await import("path");
46744
- const home2 = homedir8();
47080
+ const { homedir: homedir9, platform: platform5 } = await import("os");
47081
+ const { join: join10 } = await import("path");
47082
+ const home2 = homedir9();
46745
47083
  const dirs = [
46746
- join9(home2, ".claude/projects"),
46747
- join9(home2, ".codex/sessions"),
46748
- join9(home2, ".cursor/ai-tracking"),
46749
- join9(home2, ".gemini"),
47084
+ join10(home2, ".claude/projects"),
47085
+ join10(home2, ".codex/sessions"),
47086
+ join10(home2, ".cursor/ai-tracking"),
47087
+ join10(home2, ".gemini"),
46750
47088
  ...platform5() === "darwin" ? [
46751
- join9(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46752
- join9(home2, "Library/Application Support/Claude")
47089
+ join10(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
47090
+ join10(home2, "Library/Application Support/Claude")
46753
47091
  ] : [
46754
- join9(home2, ".config/Cursor/User/workspaceStorage")
47092
+ join10(home2, ".config/Cursor/User/workspaceStorage")
46755
47093
  ]
46756
- ].filter((p) => existsSync7(p) && statSync2(p).isDirectory());
47094
+ ].filter((p) => existsSync8(p) && statSync2(p).isDirectory());
46757
47095
  setPhase("watching", `Watching ${dirs.length} directories`);
46758
47096
  const watcher = chokidar.watch(dirs, {
46759
47097
  persistent: true,
@@ -46820,37 +47158,37 @@ var watch_exports = {};
46820
47158
  __export(watch_exports, {
46821
47159
  watchForever: () => watchForever
46822
47160
  });
46823
- import { existsSync as existsSync8 } from "fs";
46824
- import { homedir as homedir7, platform as platform3 } from "os";
46825
- import { join as join8 } from "path";
47161
+ import { existsSync as existsSync9 } from "fs";
47162
+ import { homedir as homedir8, platform as platform3 } from "os";
47163
+ import { join as join9 } from "path";
46826
47164
  function resolveWatchDirs() {
46827
- const home2 = homedir7();
46828
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join8(home2, ".config");
46829
- const xdgData = process.env.XDG_DATA_HOME ?? join8(home2, ".local/share");
47165
+ const home2 = homedir8();
47166
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join9(home2, ".config");
47167
+ const xdgData = process.env.XDG_DATA_HOME ?? join9(home2, ".local/share");
46830
47168
  const candidates = [
46831
47169
  // universal (default HOME-rooted CLI data dirs)
46832
- join8(home2, ".claude/projects"),
46833
- join8(home2, ".codex/sessions"),
46834
- join8(home2, ".cursor/ai-tracking"),
46835
- join8(home2, ".gemini"),
46836
- join8(home2, ".aider"),
47170
+ join9(home2, ".claude/projects"),
47171
+ join9(home2, ".codex/sessions"),
47172
+ join9(home2, ".cursor/ai-tracking"),
47173
+ join9(home2, ".gemini"),
47174
+ join9(home2, ".aider"),
46837
47175
  // XDG / Linux
46838
- join8(xdgConfig, "claude/projects"),
46839
- join8(xdgConfig, "codex/sessions"),
46840
- join8(xdgConfig, "Cursor/User/workspaceStorage"),
46841
- join8(xdgConfig, "Code/User/workspaceStorage"),
46842
- join8(xdgConfig, "Code - Insiders/User/workspaceStorage"),
46843
- join8(xdgData, "claude/projects"),
47176
+ join9(xdgConfig, "claude/projects"),
47177
+ join9(xdgConfig, "codex/sessions"),
47178
+ join9(xdgConfig, "Cursor/User/workspaceStorage"),
47179
+ join9(xdgConfig, "Code/User/workspaceStorage"),
47180
+ join9(xdgConfig, "Code - Insiders/User/workspaceStorage"),
47181
+ join9(xdgData, "claude/projects"),
46844
47182
  // macOS
46845
47183
  ...platform3() === "darwin" ? [
46846
- join8(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46847
- join8(home2, "Library/Application Support/Claude"),
46848
- join8(home2, "Library/Application Support/Code/User/workspaceStorage"),
46849
- join8(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
46850
- join8(home2, "Library/Application Support/Zed")
47184
+ join9(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
47185
+ join9(home2, "Library/Application Support/Claude"),
47186
+ join9(home2, "Library/Application Support/Code/User/workspaceStorage"),
47187
+ join9(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
47188
+ join9(home2, "Library/Application Support/Zed")
46851
47189
  ] : []
46852
47190
  ];
46853
- return Array.from(new Set(candidates)).filter((p) => existsSync8(p));
47191
+ return Array.from(new Set(candidates)).filter((p) => existsSync9(p));
46854
47192
  }
46855
47193
  async function safeScan(reason) {
46856
47194
  if (scanning) {
@@ -46923,30 +47261,30 @@ import { createInterface as createInterface3 } from "readline";
46923
47261
  import { spawnSync } from "child_process";
46924
47262
  import {
46925
47263
  copyFileSync,
46926
- existsSync as existsSync5,
47264
+ existsSync as existsSync6,
46927
47265
  mkdirSync as mkdirSync2,
46928
47266
  unlinkSync,
46929
47267
  writeFileSync as writeFileSync3
46930
47268
  } from "fs";
46931
- import { homedir as homedir5, platform as platform2, userInfo } from "os";
46932
- import { dirname as dirname4, join as join4 } from "path";
47269
+ import { homedir as homedir6, platform as platform2, userInfo } from "os";
47270
+ import { dirname as dirname5, join as join5 } from "path";
46933
47271
  import { fileURLToPath as fileURLToPath2 } from "url";
46934
47272
  var SERVICE_LABEL = "ai.modelstat.agent";
46935
47273
  var SYSTEMD_UNIT = "modelstat";
46936
47274
  function home() {
46937
- return homedir5();
47275
+ return homedir6();
46938
47276
  }
46939
47277
  function stateDir() {
46940
- return join4(home(), ".modelstat");
47278
+ return join5(home(), ".modelstat");
46941
47279
  }
46942
47280
  function binDir() {
46943
- return join4(stateDir(), "bin");
47281
+ return join5(stateDir(), "bin");
46944
47282
  }
46945
47283
  function logDir() {
46946
- return join4(stateDir(), "logs");
47284
+ return join5(stateDir(), "logs");
46947
47285
  }
46948
47286
  function installedCliPath() {
46949
- return join4(binDir(), "modelstat.mjs");
47287
+ return join5(binDir(), "modelstat.mjs");
46950
47288
  }
46951
47289
  function runningCliPath() {
46952
47290
  return fileURLToPath2(import.meta.url).replace(/service\.(mjs|js|ts)$/, "cli.mjs");
@@ -46956,7 +47294,7 @@ function installBundle() {
46956
47294
  mkdirSync2(logDir(), { recursive: true });
46957
47295
  const src = runningCliPath();
46958
47296
  const dest = installedCliPath();
46959
- if (!existsSync5(src)) {
47297
+ if (!existsSync6(src)) {
46960
47298
  throw new Error(
46961
47299
  `Can't find the CLI bundle to install from (${src}). Are you running a local dev build?`
46962
47300
  );
@@ -46968,21 +47306,21 @@ function nodeBinary() {
46968
47306
  return process.execPath;
46969
47307
  }
46970
47308
  function plistPath() {
46971
- return join4(home(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
47309
+ return join5(home(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
46972
47310
  }
46973
47311
  function locateTrayExecutable() {
46974
47312
  const candidates = [
46975
- join4(home(), "Applications", "ModelstatTray.app", "Contents", "MacOS", "modelstat-tray"),
47313
+ join5(home(), "Applications", "ModelstatTray.app", "Contents", "MacOS", "modelstat-tray"),
46976
47314
  "/Applications/ModelstatTray.app/Contents/MacOS/modelstat-tray"
46977
47315
  ];
46978
47316
  for (const p of candidates) {
46979
- if (existsSync5(p)) return p;
47317
+ if (existsSync6(p)) return p;
46980
47318
  }
46981
47319
  return null;
46982
47320
  }
46983
47321
  function writePlist(cliPath) {
46984
47322
  const p = plistPath();
46985
- mkdirSync2(dirname4(p), { recursive: true });
47323
+ mkdirSync2(dirname5(p), { recursive: true });
46986
47324
  const tray = locateTrayExecutable();
46987
47325
  const programArgs = tray ? ` <string>${tray}</string>` : [
46988
47326
  ` <string>${nodeBinary()}</string>`,
@@ -47002,8 +47340,8 @@ ${programArgs}
47002
47340
  <key>KeepAlive</key>
47003
47341
  <dict><key>SuccessfulExit</key><false/></dict>
47004
47342
  <key>ThrottleInterval</key><integer>30</integer>
47005
- <key>StandardOutPath</key><string>${join4(logDir(), "out.log")}</string>
47006
- <key>StandardErrorPath</key><string>${join4(logDir(), "err.log")}</string>
47343
+ <key>StandardOutPath</key><string>${join5(logDir(), "out.log")}</string>
47344
+ <key>StandardErrorPath</key><string>${join5(logDir(), "err.log")}</string>
47007
47345
  <key>EnvironmentVariables</key>
47008
47346
  <dict>
47009
47347
  <key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
@@ -47043,7 +47381,7 @@ function macUninstall() {
47043
47381
  const target = `gui/${uid}/${SERVICE_LABEL}`;
47044
47382
  launchctl(["bootout", target]);
47045
47383
  const plist = plistPath();
47046
- if (existsSync5(plist)) {
47384
+ if (existsSync6(plist)) {
47047
47385
  try {
47048
47386
  unlinkSync(plist);
47049
47387
  } catch {
@@ -47056,12 +47394,12 @@ function macStatus() {
47056
47394
  return { running: r.ok, hint: r.ok ? "launchd managed" : "not installed" };
47057
47395
  }
47058
47396
  function systemdUnitPath() {
47059
- const xdg = process.env.XDG_CONFIG_HOME ?? join4(home(), ".config");
47060
- return join4(xdg, "systemd", "user", `${SYSTEMD_UNIT}.service`);
47397
+ const xdg = process.env.XDG_CONFIG_HOME ?? join5(home(), ".config");
47398
+ return join5(xdg, "systemd", "user", `${SYSTEMD_UNIT}.service`);
47061
47399
  }
47062
47400
  function writeSystemdUnit(cliPath) {
47063
47401
  const unitPath = systemdUnitPath();
47064
- mkdirSync2(dirname4(unitPath), { recursive: true });
47402
+ mkdirSync2(dirname5(unitPath), { recursive: true });
47065
47403
  const unit = `[Unit]
47066
47404
  Description=modelstat agent
47067
47405
  Documentation=https://modelstat.ai
@@ -47076,8 +47414,8 @@ RestartSec=10
47076
47414
  # Don't restart-storm if the service is persistently unreachable.
47077
47415
  StartLimitIntervalSec=300
47078
47416
  StartLimitBurst=10
47079
- StandardOutput=append:${join4(logDir(), "out.log")}
47080
- StandardError=append:${join4(logDir(), "err.log")}
47417
+ StandardOutput=append:${join5(logDir(), "out.log")}
47418
+ StandardError=append:${join5(logDir(), "err.log")}
47081
47419
 
47082
47420
  [Install]
47083
47421
  WantedBy=default.target
@@ -47102,7 +47440,7 @@ function linuxInstall() {
47102
47440
  function linuxUninstall() {
47103
47441
  systemctl(["disable", "--now", `${SYSTEMD_UNIT}.service`]);
47104
47442
  const unit = systemdUnitPath();
47105
- if (existsSync5(unit)) {
47443
+ if (existsSync6(unit)) {
47106
47444
  try {
47107
47445
  unlinkSync(unit);
47108
47446
  } catch {
@@ -47146,9 +47484,9 @@ function logsDir() {
47146
47484
  }
47147
47485
  function installTrayApp(sourceAppPath) {
47148
47486
  if (platform2() !== "darwin") return null;
47149
- if (!existsSync5(sourceAppPath)) return null;
47150
- const dest = join4(home(), "Applications", "ModelstatTray.app");
47151
- mkdirSync2(dirname4(dest), { recursive: true });
47487
+ if (!existsSync6(sourceAppPath)) return null;
47488
+ const dest = join5(home(), "Applications", "ModelstatTray.app");
47489
+ mkdirSync2(dirname5(dest), { recursive: true });
47152
47490
  spawnSync("rm", ["-rf", dest]);
47153
47491
  const r = spawnSync("cp", ["-R", sourceAppPath, dest], { encoding: "utf8" });
47154
47492
  if (r.status !== 0) {
@@ -47158,28 +47496,28 @@ function installTrayApp(sourceAppPath) {
47158
47496
  }
47159
47497
  function bundledTrayAppPath() {
47160
47498
  if (platform2() !== "darwin") return null;
47161
- const here2 = dirname4(fileURLToPath2(import.meta.url));
47499
+ const here2 = dirname5(fileURLToPath2(import.meta.url));
47162
47500
  const candidates = [
47163
47501
  // Pre-built .app — CI with codesigning drops one here.
47164
- join4(here2, "..", "vendor", "ModelstatTray.app"),
47502
+ join5(here2, "..", "vendor", "ModelstatTray.app"),
47165
47503
  // Local dev layout: apps/agent-dev/src/service.ts → ../../tray-mac/build/ModelstatTray.app
47166
- join4(here2, "..", "..", "tray-mac", "build", "ModelstatTray.app")
47504
+ join5(here2, "..", "..", "tray-mac", "build", "ModelstatTray.app")
47167
47505
  ];
47168
47506
  for (const c of candidates) {
47169
- if (existsSync5(c)) return c;
47507
+ if (existsSync6(c)) return c;
47170
47508
  }
47171
47509
  const sourceDirs = [
47172
- join4(here2, "..", "vendor", "tray-mac"),
47173
- join4(here2, "..", "..", "tray-mac")
47510
+ join5(here2, "..", "vendor", "tray-mac"),
47511
+ join5(here2, "..", "..", "tray-mac")
47174
47512
  ];
47175
47513
  for (const src of sourceDirs) {
47176
- const build = join4(src, "build-app.sh");
47177
- if (!existsSync5(build)) continue;
47514
+ const build = join5(src, "build-app.sh");
47515
+ if (!existsSync6(build)) continue;
47178
47516
  if (!hasSwift()) return null;
47179
47517
  const r = spawnSync("bash", [build], { cwd: src, encoding: "utf8" });
47180
47518
  if (r.status === 0) {
47181
- const app = join4(src, "build", "ModelstatTray.app");
47182
- if (existsSync5(app)) return app;
47519
+ const app = join5(src, "build", "ModelstatTray.app");
47520
+ if (existsSync6(app)) return app;
47183
47521
  }
47184
47522
  }
47185
47523
  return null;
@@ -47411,12 +47749,33 @@ async function cmdConnect(opts) {
47411
47749
  warn(`tray install skipped: ${e.message}`);
47412
47750
  }
47413
47751
  }
47752
+ step("Preparing local summariser (downloads on first run)");
47753
+ let modelReady = false;
47754
+ try {
47755
+ const { ensureLlamaModel: ensureLlamaModel2, defaultLlamaConfig: defaultLlamaConfig2 } = await Promise.resolve().then(() => (init_node2(), node_exports));
47756
+ await ensureLlamaModel2(defaultLlamaConfig2());
47757
+ modelReady = true;
47758
+ emitEvent(opts, "summariser_model_ready", {});
47759
+ ok("summariser model on disk");
47760
+ } catch (e) {
47761
+ emitEvent(opts, "summariser_model_failed", {
47762
+ error: e.message
47763
+ });
47764
+ warn(`couldn't prepare summariser model: ${e.message}`);
47765
+ warn(
47766
+ "the background service will retry the download on its first scan"
47767
+ );
47768
+ }
47414
47769
  step("Installing background service so the agent survives reboots");
47415
47770
  let serviceOk = false;
47416
47771
  try {
47417
47772
  const svc = installService();
47418
47773
  serviceOk = true;
47419
- emitEvent(opts, "service_installed", { path: svc.path, logs: svc.logs });
47774
+ emitEvent(opts, "service_installed", {
47775
+ path: svc.path,
47776
+ logs: svc.logs,
47777
+ summariser_ready: modelReady
47778
+ });
47420
47779
  ok(`${platform4() === "darwin" ? "launchd" : "systemd --user"}: ${svc.path}`);
47421
47780
  } catch (e) {
47422
47781
  emitEvent(opts, "service_install_failed", { error: e.message });
@@ -47485,6 +47844,9 @@ async function cmdDiscover() {
47485
47844
  console.log("\u2713 reported to backend");
47486
47845
  }
47487
47846
  async function cmdScan() {
47847
+ const { preflightSummariser: preflightSummariser2 } = await Promise.resolve().then(() => (init_pipeline2(), pipeline_exports));
47848
+ const sample = await preflightSummariser2();
47849
+ console.log(`[modelstat] summariser preflight ok: "${sample}"`);
47488
47850
  const r = await scanAll();
47489
47851
  console.log();
47490
47852
  console.log(