modelstat 0.0.25 → 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 +474 -121
- package/dist/cli.mjs.map +1 -1
- package/package.json +4 -1
- package/scripts/postinstall.mjs +120 -0
- package/vendor/tray-mac/Sources/ModelstatTray/main.swift +24 -3
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:
|
|
21134
|
+
var { writeFile, readFile, mkdir: mkdir2 } = __require("fs/promises");
|
|
21135
|
+
var { dirname: dirname8, 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
|
|
21366
|
+
await mkdir2(dirname8(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 = "
|
|
44250
|
-
SUMMARISER_MAX_TOKENS =
|
|
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
|
|
44368
|
-
Write
|
|
44369
|
-
|
|
44370
|
-
|
|
44371
|
-
|
|
44372
|
-
}
|
|
44373
|
-
|
|
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
|
-
|
|
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
|
|
44920
|
+
function bundledAdapters() {
|
|
44619
44921
|
return {
|
|
44620
44922
|
embed: async () => [],
|
|
44621
|
-
|
|
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
|
|
44632
|
-
const
|
|
44929
|
+
const ollamaCfg = defaultOllamaConfig();
|
|
44930
|
+
const ollamaUp = await probeOllama(ollamaCfg.baseUrl);
|
|
44633
44931
|
probed = true;
|
|
44634
|
-
if (
|
|
44635
|
-
console.log(
|
|
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(
|
|
44638
|
-
summarize: ollamaSummarize(
|
|
44937
|
+
embed: ollamaEmbed(ollamaCfg),
|
|
44938
|
+
summarize: ollamaSummarize(ollamaCfg),
|
|
44639
44939
|
tokenize: ollamaTokenize()
|
|
44640
44940
|
};
|
|
44641
|
-
|
|
44642
|
-
|
|
44643
|
-
|
|
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
|
|
44666
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
45018
|
+
const months = await readdir(join4(base, y)).catch(() => []);
|
|
44699
45019
|
for (const m of months) {
|
|
44700
|
-
const days = await readdir(
|
|
45020
|
+
const days = await readdir(join4(base, y, m)).catch(() => []);
|
|
44701
45021
|
for (const d of days) {
|
|
44702
|
-
const files = await readdir(
|
|
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 =
|
|
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
|
|
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
|
|
44797
|
-
import { join as
|
|
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 =
|
|
44899
|
-
LOCK_FILE =
|
|
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
|
|
45956
|
+
const dirname8 = sysPath.dirname(file);
|
|
45637
45957
|
const basename4 = sysPath.basename(file);
|
|
45638
|
-
const parent = this.fsw._getWatchedDir(
|
|
45958
|
+
const parent = this.fsw._getWatchedDir(dirname8);
|
|
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(
|
|
45985
|
+
this.fsw._remove(dirname8, basename4);
|
|
45666
45986
|
}
|
|
45667
45987
|
} else if (parent.has(basename4)) {
|
|
45668
45988
|
const at = newStats.atimeMs;
|
|
@@ -46626,7 +46946,7 @@ __export(daemon_exports, {
|
|
|
46626
46946
|
setProgress: () => setProgress,
|
|
46627
46947
|
setQueue: () => setQueue
|
|
46628
46948
|
});
|
|
46629
|
-
import { existsSync as
|
|
46949
|
+
import { existsSync as existsSync8, statSync as statSync2 } from "fs";
|
|
46630
46950
|
function setPhase(phase, message) {
|
|
46631
46951
|
status.phase = phase;
|
|
46632
46952
|
status.message = message ?? null;
|
|
@@ -46745,24 +47065,33 @@ async function runDaemon(opts = {}) {
|
|
|
46745
47065
|
const hb = setInterval(() => void sendHeartbeat(), HEARTBEAT_INTERVAL_MS);
|
|
46746
47066
|
hb.unref();
|
|
46747
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
|
+
}
|
|
46748
47077
|
await runDiscovery();
|
|
46749
47078
|
await runScanCycle("startup");
|
|
46750
47079
|
const chokidar = (await Promise.resolve().then(() => (init_esm2(), esm_exports))).default;
|
|
46751
|
-
const { homedir:
|
|
46752
|
-
const { join:
|
|
46753
|
-
const home2 =
|
|
47080
|
+
const { homedir: homedir9, platform: platform5 } = await import("os");
|
|
47081
|
+
const { join: join10 } = await import("path");
|
|
47082
|
+
const home2 = homedir9();
|
|
46754
47083
|
const dirs = [
|
|
46755
|
-
|
|
46756
|
-
|
|
46757
|
-
|
|
46758
|
-
|
|
47084
|
+
join10(home2, ".claude/projects"),
|
|
47085
|
+
join10(home2, ".codex/sessions"),
|
|
47086
|
+
join10(home2, ".cursor/ai-tracking"),
|
|
47087
|
+
join10(home2, ".gemini"),
|
|
46759
47088
|
...platform5() === "darwin" ? [
|
|
46760
|
-
|
|
46761
|
-
|
|
47089
|
+
join10(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
|
|
47090
|
+
join10(home2, "Library/Application Support/Claude")
|
|
46762
47091
|
] : [
|
|
46763
|
-
|
|
47092
|
+
join10(home2, ".config/Cursor/User/workspaceStorage")
|
|
46764
47093
|
]
|
|
46765
|
-
].filter((p) =>
|
|
47094
|
+
].filter((p) => existsSync8(p) && statSync2(p).isDirectory());
|
|
46766
47095
|
setPhase("watching", `Watching ${dirs.length} directories`);
|
|
46767
47096
|
const watcher = chokidar.watch(dirs, {
|
|
46768
47097
|
persistent: true,
|
|
@@ -46829,37 +47158,37 @@ var watch_exports = {};
|
|
|
46829
47158
|
__export(watch_exports, {
|
|
46830
47159
|
watchForever: () => watchForever
|
|
46831
47160
|
});
|
|
46832
|
-
import { existsSync as
|
|
46833
|
-
import { homedir as
|
|
46834
|
-
import { join as
|
|
47161
|
+
import { existsSync as existsSync9 } from "fs";
|
|
47162
|
+
import { homedir as homedir8, platform as platform3 } from "os";
|
|
47163
|
+
import { join as join9 } from "path";
|
|
46835
47164
|
function resolveWatchDirs() {
|
|
46836
|
-
const home2 =
|
|
46837
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
46838
|
-
const xdgData = process.env.XDG_DATA_HOME ??
|
|
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");
|
|
46839
47168
|
const candidates = [
|
|
46840
47169
|
// universal (default HOME-rooted CLI data dirs)
|
|
46841
|
-
|
|
46842
|
-
|
|
46843
|
-
|
|
46844
|
-
|
|
46845
|
-
|
|
47170
|
+
join9(home2, ".claude/projects"),
|
|
47171
|
+
join9(home2, ".codex/sessions"),
|
|
47172
|
+
join9(home2, ".cursor/ai-tracking"),
|
|
47173
|
+
join9(home2, ".gemini"),
|
|
47174
|
+
join9(home2, ".aider"),
|
|
46846
47175
|
// XDG / Linux
|
|
46847
|
-
|
|
46848
|
-
|
|
46849
|
-
|
|
46850
|
-
|
|
46851
|
-
|
|
46852
|
-
|
|
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"),
|
|
46853
47182
|
// macOS
|
|
46854
47183
|
...platform3() === "darwin" ? [
|
|
46855
|
-
|
|
46856
|
-
|
|
46857
|
-
|
|
46858
|
-
|
|
46859
|
-
|
|
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")
|
|
46860
47189
|
] : []
|
|
46861
47190
|
];
|
|
46862
|
-
return Array.from(new Set(candidates)).filter((p) =>
|
|
47191
|
+
return Array.from(new Set(candidates)).filter((p) => existsSync9(p));
|
|
46863
47192
|
}
|
|
46864
47193
|
async function safeScan(reason) {
|
|
46865
47194
|
if (scanning) {
|
|
@@ -46932,30 +47261,30 @@ import { createInterface as createInterface3 } from "readline";
|
|
|
46932
47261
|
import { spawnSync } from "child_process";
|
|
46933
47262
|
import {
|
|
46934
47263
|
copyFileSync,
|
|
46935
|
-
existsSync as
|
|
47264
|
+
existsSync as existsSync6,
|
|
46936
47265
|
mkdirSync as mkdirSync2,
|
|
46937
47266
|
unlinkSync,
|
|
46938
47267
|
writeFileSync as writeFileSync3
|
|
46939
47268
|
} from "fs";
|
|
46940
|
-
import { homedir as
|
|
46941
|
-
import { dirname as
|
|
47269
|
+
import { homedir as homedir6, platform as platform2, userInfo } from "os";
|
|
47270
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
46942
47271
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
46943
47272
|
var SERVICE_LABEL = "ai.modelstat.agent";
|
|
46944
47273
|
var SYSTEMD_UNIT = "modelstat";
|
|
46945
47274
|
function home() {
|
|
46946
|
-
return
|
|
47275
|
+
return homedir6();
|
|
46947
47276
|
}
|
|
46948
47277
|
function stateDir() {
|
|
46949
|
-
return
|
|
47278
|
+
return join5(home(), ".modelstat");
|
|
46950
47279
|
}
|
|
46951
47280
|
function binDir() {
|
|
46952
|
-
return
|
|
47281
|
+
return join5(stateDir(), "bin");
|
|
46953
47282
|
}
|
|
46954
47283
|
function logDir() {
|
|
46955
|
-
return
|
|
47284
|
+
return join5(stateDir(), "logs");
|
|
46956
47285
|
}
|
|
46957
47286
|
function installedCliPath() {
|
|
46958
|
-
return
|
|
47287
|
+
return join5(binDir(), "modelstat.mjs");
|
|
46959
47288
|
}
|
|
46960
47289
|
function runningCliPath() {
|
|
46961
47290
|
return fileURLToPath2(import.meta.url).replace(/service\.(mjs|js|ts)$/, "cli.mjs");
|
|
@@ -46965,7 +47294,7 @@ function installBundle() {
|
|
|
46965
47294
|
mkdirSync2(logDir(), { recursive: true });
|
|
46966
47295
|
const src = runningCliPath();
|
|
46967
47296
|
const dest = installedCliPath();
|
|
46968
|
-
if (!
|
|
47297
|
+
if (!existsSync6(src)) {
|
|
46969
47298
|
throw new Error(
|
|
46970
47299
|
`Can't find the CLI bundle to install from (${src}). Are you running a local dev build?`
|
|
46971
47300
|
);
|
|
@@ -46977,21 +47306,21 @@ function nodeBinary() {
|
|
|
46977
47306
|
return process.execPath;
|
|
46978
47307
|
}
|
|
46979
47308
|
function plistPath() {
|
|
46980
|
-
return
|
|
47309
|
+
return join5(home(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
46981
47310
|
}
|
|
46982
47311
|
function locateTrayExecutable() {
|
|
46983
47312
|
const candidates = [
|
|
46984
|
-
|
|
47313
|
+
join5(home(), "Applications", "ModelstatTray.app", "Contents", "MacOS", "modelstat-tray"),
|
|
46985
47314
|
"/Applications/ModelstatTray.app/Contents/MacOS/modelstat-tray"
|
|
46986
47315
|
];
|
|
46987
47316
|
for (const p of candidates) {
|
|
46988
|
-
if (
|
|
47317
|
+
if (existsSync6(p)) return p;
|
|
46989
47318
|
}
|
|
46990
47319
|
return null;
|
|
46991
47320
|
}
|
|
46992
47321
|
function writePlist(cliPath) {
|
|
46993
47322
|
const p = plistPath();
|
|
46994
|
-
mkdirSync2(
|
|
47323
|
+
mkdirSync2(dirname5(p), { recursive: true });
|
|
46995
47324
|
const tray = locateTrayExecutable();
|
|
46996
47325
|
const programArgs = tray ? ` <string>${tray}</string>` : [
|
|
46997
47326
|
` <string>${nodeBinary()}</string>`,
|
|
@@ -47011,8 +47340,8 @@ ${programArgs}
|
|
|
47011
47340
|
<key>KeepAlive</key>
|
|
47012
47341
|
<dict><key>SuccessfulExit</key><false/></dict>
|
|
47013
47342
|
<key>ThrottleInterval</key><integer>30</integer>
|
|
47014
|
-
<key>StandardOutPath</key><string>${
|
|
47015
|
-
<key>StandardErrorPath</key><string>${
|
|
47343
|
+
<key>StandardOutPath</key><string>${join5(logDir(), "out.log")}</string>
|
|
47344
|
+
<key>StandardErrorPath</key><string>${join5(logDir(), "err.log")}</string>
|
|
47016
47345
|
<key>EnvironmentVariables</key>
|
|
47017
47346
|
<dict>
|
|
47018
47347
|
<key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
@@ -47052,7 +47381,7 @@ function macUninstall() {
|
|
|
47052
47381
|
const target = `gui/${uid}/${SERVICE_LABEL}`;
|
|
47053
47382
|
launchctl(["bootout", target]);
|
|
47054
47383
|
const plist = plistPath();
|
|
47055
|
-
if (
|
|
47384
|
+
if (existsSync6(plist)) {
|
|
47056
47385
|
try {
|
|
47057
47386
|
unlinkSync(plist);
|
|
47058
47387
|
} catch {
|
|
@@ -47065,12 +47394,12 @@ function macStatus() {
|
|
|
47065
47394
|
return { running: r.ok, hint: r.ok ? "launchd managed" : "not installed" };
|
|
47066
47395
|
}
|
|
47067
47396
|
function systemdUnitPath() {
|
|
47068
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
47069
|
-
return
|
|
47397
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? join5(home(), ".config");
|
|
47398
|
+
return join5(xdg, "systemd", "user", `${SYSTEMD_UNIT}.service`);
|
|
47070
47399
|
}
|
|
47071
47400
|
function writeSystemdUnit(cliPath) {
|
|
47072
47401
|
const unitPath = systemdUnitPath();
|
|
47073
|
-
mkdirSync2(
|
|
47402
|
+
mkdirSync2(dirname5(unitPath), { recursive: true });
|
|
47074
47403
|
const unit = `[Unit]
|
|
47075
47404
|
Description=modelstat agent
|
|
47076
47405
|
Documentation=https://modelstat.ai
|
|
@@ -47085,8 +47414,8 @@ RestartSec=10
|
|
|
47085
47414
|
# Don't restart-storm if the service is persistently unreachable.
|
|
47086
47415
|
StartLimitIntervalSec=300
|
|
47087
47416
|
StartLimitBurst=10
|
|
47088
|
-
StandardOutput=append:${
|
|
47089
|
-
StandardError=append:${
|
|
47417
|
+
StandardOutput=append:${join5(logDir(), "out.log")}
|
|
47418
|
+
StandardError=append:${join5(logDir(), "err.log")}
|
|
47090
47419
|
|
|
47091
47420
|
[Install]
|
|
47092
47421
|
WantedBy=default.target
|
|
@@ -47111,7 +47440,7 @@ function linuxInstall() {
|
|
|
47111
47440
|
function linuxUninstall() {
|
|
47112
47441
|
systemctl(["disable", "--now", `${SYSTEMD_UNIT}.service`]);
|
|
47113
47442
|
const unit = systemdUnitPath();
|
|
47114
|
-
if (
|
|
47443
|
+
if (existsSync6(unit)) {
|
|
47115
47444
|
try {
|
|
47116
47445
|
unlinkSync(unit);
|
|
47117
47446
|
} catch {
|
|
@@ -47155,9 +47484,9 @@ function logsDir() {
|
|
|
47155
47484
|
}
|
|
47156
47485
|
function installTrayApp(sourceAppPath) {
|
|
47157
47486
|
if (platform2() !== "darwin") return null;
|
|
47158
|
-
if (!
|
|
47159
|
-
const dest =
|
|
47160
|
-
mkdirSync2(
|
|
47487
|
+
if (!existsSync6(sourceAppPath)) return null;
|
|
47488
|
+
const dest = join5(home(), "Applications", "ModelstatTray.app");
|
|
47489
|
+
mkdirSync2(dirname5(dest), { recursive: true });
|
|
47161
47490
|
spawnSync("rm", ["-rf", dest]);
|
|
47162
47491
|
const r = spawnSync("cp", ["-R", sourceAppPath, dest], { encoding: "utf8" });
|
|
47163
47492
|
if (r.status !== 0) {
|
|
@@ -47167,28 +47496,28 @@ function installTrayApp(sourceAppPath) {
|
|
|
47167
47496
|
}
|
|
47168
47497
|
function bundledTrayAppPath() {
|
|
47169
47498
|
if (platform2() !== "darwin") return null;
|
|
47170
|
-
const here2 =
|
|
47499
|
+
const here2 = dirname5(fileURLToPath2(import.meta.url));
|
|
47171
47500
|
const candidates = [
|
|
47172
47501
|
// Pre-built .app — CI with codesigning drops one here.
|
|
47173
|
-
|
|
47502
|
+
join5(here2, "..", "vendor", "ModelstatTray.app"),
|
|
47174
47503
|
// Local dev layout: apps/agent-dev/src/service.ts → ../../tray-mac/build/ModelstatTray.app
|
|
47175
|
-
|
|
47504
|
+
join5(here2, "..", "..", "tray-mac", "build", "ModelstatTray.app")
|
|
47176
47505
|
];
|
|
47177
47506
|
for (const c of candidates) {
|
|
47178
|
-
if (
|
|
47507
|
+
if (existsSync6(c)) return c;
|
|
47179
47508
|
}
|
|
47180
47509
|
const sourceDirs = [
|
|
47181
|
-
|
|
47182
|
-
|
|
47510
|
+
join5(here2, "..", "vendor", "tray-mac"),
|
|
47511
|
+
join5(here2, "..", "..", "tray-mac")
|
|
47183
47512
|
];
|
|
47184
47513
|
for (const src of sourceDirs) {
|
|
47185
|
-
const build =
|
|
47186
|
-
if (!
|
|
47514
|
+
const build = join5(src, "build-app.sh");
|
|
47515
|
+
if (!existsSync6(build)) continue;
|
|
47187
47516
|
if (!hasSwift()) return null;
|
|
47188
47517
|
const r = spawnSync("bash", [build], { cwd: src, encoding: "utf8" });
|
|
47189
47518
|
if (r.status === 0) {
|
|
47190
|
-
const app =
|
|
47191
|
-
if (
|
|
47519
|
+
const app = join5(src, "build", "ModelstatTray.app");
|
|
47520
|
+
if (existsSync6(app)) return app;
|
|
47192
47521
|
}
|
|
47193
47522
|
}
|
|
47194
47523
|
return null;
|
|
@@ -47420,12 +47749,33 @@ async function cmdConnect(opts) {
|
|
|
47420
47749
|
warn(`tray install skipped: ${e.message}`);
|
|
47421
47750
|
}
|
|
47422
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
|
+
}
|
|
47423
47769
|
step("Installing background service so the agent survives reboots");
|
|
47424
47770
|
let serviceOk = false;
|
|
47425
47771
|
try {
|
|
47426
47772
|
const svc = installService();
|
|
47427
47773
|
serviceOk = true;
|
|
47428
|
-
emitEvent(opts, "service_installed", {
|
|
47774
|
+
emitEvent(opts, "service_installed", {
|
|
47775
|
+
path: svc.path,
|
|
47776
|
+
logs: svc.logs,
|
|
47777
|
+
summariser_ready: modelReady
|
|
47778
|
+
});
|
|
47429
47779
|
ok(`${platform4() === "darwin" ? "launchd" : "systemd --user"}: ${svc.path}`);
|
|
47430
47780
|
} catch (e) {
|
|
47431
47781
|
emitEvent(opts, "service_install_failed", { error: e.message });
|
|
@@ -47494,6 +47844,9 @@ async function cmdDiscover() {
|
|
|
47494
47844
|
console.log("\u2713 reported to backend");
|
|
47495
47845
|
}
|
|
47496
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}"`);
|
|
47497
47850
|
const r = await scanAll();
|
|
47498
47851
|
console.log();
|
|
47499
47852
|
console.log(
|