modelstat 0.0.25 → 0.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +495 -122
- 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: dirname9, resolve: resolve6 } = __require("path");
|
|
21136
21136
|
var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("timers");
|
|
21137
21137
|
var { InvalidArgumentError, UndiciError } = require_errors();
|
|
21138
21138
|
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
|
|
@@ -21363,7 +21363,7 @@ var require_snapshot_recorder = __commonJS({
|
|
|
21363
21363
|
throw new InvalidArgumentError("Snapshot path is required");
|
|
21364
21364
|
}
|
|
21365
21365
|
const resolvedPath = resolve6(path5);
|
|
21366
|
-
await
|
|
21366
|
+
await mkdir2(dirname9(resolvedPath), { recursive: true });
|
|
21367
21367
|
const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
|
|
21368
21368
|
hash,
|
|
21369
21369
|
snapshot
|
|
@@ -44240,14 +44240,15 @@ var init_queue = __esm({
|
|
|
44240
44240
|
});
|
|
44241
44241
|
|
|
44242
44242
|
// ../../packages/companion-core/src/pipeline/prompts.ts
|
|
44243
|
-
var OLLAMA_CHAT_MODEL, OLLAMA_EMBED_MODEL, SUMMARISER_SYSTEM_PROMPT, SUMMARISER_MAX_TOKENS, SUMMARISER_TEMPERATURE, QWEN_CHARS_PER_TOKEN;
|
|
44243
|
+
var OLLAMA_CHAT_MODEL, OLLAMA_EMBED_MODEL, SUMMARISER_SYSTEM_PROMPT, SUMMARISER_MAX_TOKENS, ABSTRACT_OUTPUT_MAX_CHARS, SUMMARISER_TEMPERATURE, QWEN_CHARS_PER_TOKEN;
|
|
44244
44244
|
var init_prompts = __esm({
|
|
44245
44245
|
"../../packages/companion-core/src/pipeline/prompts.ts"() {
|
|
44246
44246
|
"use strict";
|
|
44247
44247
|
OLLAMA_CHAT_MODEL = "qwen3.5:0.8b";
|
|
44248
44248
|
OLLAMA_EMBED_MODEL = "bge-small-en-v1.5";
|
|
44249
|
-
SUMMARISER_SYSTEM_PROMPT = "
|
|
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 dirname9 = sysPath.dirname(file);
|
|
45637
45957
|
const basename4 = sysPath.basename(file);
|
|
45638
|
-
const parent = this.fsw._getWatchedDir(
|
|
45958
|
+
const parent = this.fsw._getWatchedDir(dirname9);
|
|
45639
45959
|
let prevStats = stats;
|
|
45640
45960
|
if (parent.has(basename4))
|
|
45641
45961
|
return;
|
|
@@ -45662,7 +45982,7 @@ var init_handler = __esm({
|
|
|
45662
45982
|
prevStats = newStats2;
|
|
45663
45983
|
}
|
|
45664
45984
|
} catch (error) {
|
|
45665
|
-
this.fsw._remove(
|
|
45985
|
+
this.fsw._remove(dirname9, basename4);
|
|
45666
45986
|
}
|
|
45667
45987
|
} else if (parent.has(basename4)) {
|
|
45668
45988
|
const at = newStats.atimeMs;
|
|
@@ -46626,7 +46946,10 @@ __export(daemon_exports, {
|
|
|
46626
46946
|
setProgress: () => setProgress,
|
|
46627
46947
|
setQueue: () => setQueue
|
|
46628
46948
|
});
|
|
46629
|
-
import { existsSync as
|
|
46949
|
+
import { existsSync as existsSync8, statSync as statSync2 } from "fs";
|
|
46950
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
46951
|
+
import { dirname as dirname8, join as join9 } from "path";
|
|
46952
|
+
import { fileURLToPath as __agentFileURLToPath } from "url";
|
|
46630
46953
|
function setPhase(phase, message) {
|
|
46631
46954
|
status.phase = phase;
|
|
46632
46955
|
status.message = message ?? null;
|
|
@@ -46745,24 +47068,33 @@ async function runDaemon(opts = {}) {
|
|
|
46745
47068
|
const hb = setInterval(() => void sendHeartbeat(), HEARTBEAT_INTERVAL_MS);
|
|
46746
47069
|
hb.unref();
|
|
46747
47070
|
void sendHeartbeat();
|
|
47071
|
+
try {
|
|
47072
|
+
setPhase("starting", "Preflight: summariser");
|
|
47073
|
+
const { preflightSummariser: preflightSummariser2 } = await Promise.resolve().then(() => (init_pipeline2(), pipeline_exports));
|
|
47074
|
+
const sample = await preflightSummariser2();
|
|
47075
|
+
console.log(`[modelstat] summariser preflight ok: "${sample}"`);
|
|
47076
|
+
} catch (err) {
|
|
47077
|
+
setPhase("error", `summariser preflight failed: ${err.message}`);
|
|
47078
|
+
throw err;
|
|
47079
|
+
}
|
|
46748
47080
|
await runDiscovery();
|
|
46749
47081
|
await runScanCycle("startup");
|
|
46750
47082
|
const chokidar = (await Promise.resolve().then(() => (init_esm2(), esm_exports))).default;
|
|
46751
|
-
const { homedir:
|
|
46752
|
-
const { join:
|
|
46753
|
-
const home2 =
|
|
47083
|
+
const { homedir: homedir9, platform: platform5 } = await import("os");
|
|
47084
|
+
const { join: join11 } = await import("path");
|
|
47085
|
+
const home2 = homedir9();
|
|
46754
47086
|
const dirs = [
|
|
46755
|
-
|
|
46756
|
-
|
|
46757
|
-
|
|
46758
|
-
|
|
47087
|
+
join11(home2, ".claude/projects"),
|
|
47088
|
+
join11(home2, ".codex/sessions"),
|
|
47089
|
+
join11(home2, ".cursor/ai-tracking"),
|
|
47090
|
+
join11(home2, ".gemini"),
|
|
46759
47091
|
...platform5() === "darwin" ? [
|
|
46760
|
-
|
|
46761
|
-
|
|
47092
|
+
join11(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
|
|
47093
|
+
join11(home2, "Library/Application Support/Claude")
|
|
46762
47094
|
] : [
|
|
46763
|
-
|
|
47095
|
+
join11(home2, ".config/Cursor/User/workspaceStorage")
|
|
46764
47096
|
]
|
|
46765
|
-
].filter((p) =>
|
|
47097
|
+
].filter((p) => existsSync8(p) && statSync2(p).isDirectory());
|
|
46766
47098
|
setPhase("watching", `Watching ${dirs.length} directories`);
|
|
46767
47099
|
const watcher = chokidar.watch(dirs, {
|
|
46768
47100
|
persistent: true,
|
|
@@ -46809,7 +47141,24 @@ var init_daemon = __esm({
|
|
|
46809
47141
|
init_config2();
|
|
46810
47142
|
init_lock();
|
|
46811
47143
|
init_scan();
|
|
46812
|
-
AGENT_VERSION2 =
|
|
47144
|
+
AGENT_VERSION2 = (() => {
|
|
47145
|
+
try {
|
|
47146
|
+
const here2 = dirname8(__agentFileURLToPath(import.meta.url));
|
|
47147
|
+
const candidates = [
|
|
47148
|
+
join9(here2, "..", "package.json"),
|
|
47149
|
+
join9(here2, "..", "..", "package.json")
|
|
47150
|
+
];
|
|
47151
|
+
for (const c of candidates) {
|
|
47152
|
+
try {
|
|
47153
|
+
const v = JSON.parse(readFileSync4(c, "utf8")).version;
|
|
47154
|
+
if (typeof v === "string") return `agent-${v}`;
|
|
47155
|
+
} catch {
|
|
47156
|
+
}
|
|
47157
|
+
}
|
|
47158
|
+
} catch {
|
|
47159
|
+
}
|
|
47160
|
+
return "agent-unknown";
|
|
47161
|
+
})();
|
|
46813
47162
|
HEARTBEAT_INTERVAL_MS = 1e4;
|
|
46814
47163
|
SCAN_INTERVAL_MS = 5 * 60 * 1e3;
|
|
46815
47164
|
status = {
|
|
@@ -46829,37 +47178,37 @@ var watch_exports = {};
|
|
|
46829
47178
|
__export(watch_exports, {
|
|
46830
47179
|
watchForever: () => watchForever
|
|
46831
47180
|
});
|
|
46832
|
-
import { existsSync as
|
|
46833
|
-
import { homedir as
|
|
46834
|
-
import { join as
|
|
47181
|
+
import { existsSync as existsSync9 } from "fs";
|
|
47182
|
+
import { homedir as homedir8, platform as platform3 } from "os";
|
|
47183
|
+
import { join as join10 } from "path";
|
|
46835
47184
|
function resolveWatchDirs() {
|
|
46836
|
-
const home2 =
|
|
46837
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
46838
|
-
const xdgData = process.env.XDG_DATA_HOME ??
|
|
47185
|
+
const home2 = homedir8();
|
|
47186
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join10(home2, ".config");
|
|
47187
|
+
const xdgData = process.env.XDG_DATA_HOME ?? join10(home2, ".local/share");
|
|
46839
47188
|
const candidates = [
|
|
46840
47189
|
// universal (default HOME-rooted CLI data dirs)
|
|
46841
|
-
|
|
46842
|
-
|
|
46843
|
-
|
|
46844
|
-
|
|
46845
|
-
|
|
47190
|
+
join10(home2, ".claude/projects"),
|
|
47191
|
+
join10(home2, ".codex/sessions"),
|
|
47192
|
+
join10(home2, ".cursor/ai-tracking"),
|
|
47193
|
+
join10(home2, ".gemini"),
|
|
47194
|
+
join10(home2, ".aider"),
|
|
46846
47195
|
// XDG / Linux
|
|
46847
|
-
|
|
46848
|
-
|
|
46849
|
-
|
|
46850
|
-
|
|
46851
|
-
|
|
46852
|
-
|
|
47196
|
+
join10(xdgConfig, "claude/projects"),
|
|
47197
|
+
join10(xdgConfig, "codex/sessions"),
|
|
47198
|
+
join10(xdgConfig, "Cursor/User/workspaceStorage"),
|
|
47199
|
+
join10(xdgConfig, "Code/User/workspaceStorage"),
|
|
47200
|
+
join10(xdgConfig, "Code - Insiders/User/workspaceStorage"),
|
|
47201
|
+
join10(xdgData, "claude/projects"),
|
|
46853
47202
|
// macOS
|
|
46854
47203
|
...platform3() === "darwin" ? [
|
|
46855
|
-
|
|
46856
|
-
|
|
46857
|
-
|
|
46858
|
-
|
|
46859
|
-
|
|
47204
|
+
join10(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
|
|
47205
|
+
join10(home2, "Library/Application Support/Claude"),
|
|
47206
|
+
join10(home2, "Library/Application Support/Code/User/workspaceStorage"),
|
|
47207
|
+
join10(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
|
|
47208
|
+
join10(home2, "Library/Application Support/Zed")
|
|
46860
47209
|
] : []
|
|
46861
47210
|
];
|
|
46862
|
-
return Array.from(new Set(candidates)).filter((p) =>
|
|
47211
|
+
return Array.from(new Set(candidates)).filter((p) => existsSync9(p));
|
|
46863
47212
|
}
|
|
46864
47213
|
async function safeScan(reason) {
|
|
46865
47214
|
if (scanning) {
|
|
@@ -46932,30 +47281,30 @@ import { createInterface as createInterface3 } from "readline";
|
|
|
46932
47281
|
import { spawnSync } from "child_process";
|
|
46933
47282
|
import {
|
|
46934
47283
|
copyFileSync,
|
|
46935
|
-
existsSync as
|
|
47284
|
+
existsSync as existsSync6,
|
|
46936
47285
|
mkdirSync as mkdirSync2,
|
|
46937
47286
|
unlinkSync,
|
|
46938
47287
|
writeFileSync as writeFileSync3
|
|
46939
47288
|
} from "fs";
|
|
46940
|
-
import { homedir as
|
|
46941
|
-
import { dirname as
|
|
47289
|
+
import { homedir as homedir6, platform as platform2, userInfo } from "os";
|
|
47290
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
46942
47291
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
46943
47292
|
var SERVICE_LABEL = "ai.modelstat.agent";
|
|
46944
47293
|
var SYSTEMD_UNIT = "modelstat";
|
|
46945
47294
|
function home() {
|
|
46946
|
-
return
|
|
47295
|
+
return homedir6();
|
|
46947
47296
|
}
|
|
46948
47297
|
function stateDir() {
|
|
46949
|
-
return
|
|
47298
|
+
return join5(home(), ".modelstat");
|
|
46950
47299
|
}
|
|
46951
47300
|
function binDir() {
|
|
46952
|
-
return
|
|
47301
|
+
return join5(stateDir(), "bin");
|
|
46953
47302
|
}
|
|
46954
47303
|
function logDir() {
|
|
46955
|
-
return
|
|
47304
|
+
return join5(stateDir(), "logs");
|
|
46956
47305
|
}
|
|
46957
47306
|
function installedCliPath() {
|
|
46958
|
-
return
|
|
47307
|
+
return join5(binDir(), "modelstat.mjs");
|
|
46959
47308
|
}
|
|
46960
47309
|
function runningCliPath() {
|
|
46961
47310
|
return fileURLToPath2(import.meta.url).replace(/service\.(mjs|js|ts)$/, "cli.mjs");
|
|
@@ -46965,7 +47314,7 @@ function installBundle() {
|
|
|
46965
47314
|
mkdirSync2(logDir(), { recursive: true });
|
|
46966
47315
|
const src = runningCliPath();
|
|
46967
47316
|
const dest = installedCliPath();
|
|
46968
|
-
if (!
|
|
47317
|
+
if (!existsSync6(src)) {
|
|
46969
47318
|
throw new Error(
|
|
46970
47319
|
`Can't find the CLI bundle to install from (${src}). Are you running a local dev build?`
|
|
46971
47320
|
);
|
|
@@ -46977,21 +47326,21 @@ function nodeBinary() {
|
|
|
46977
47326
|
return process.execPath;
|
|
46978
47327
|
}
|
|
46979
47328
|
function plistPath() {
|
|
46980
|
-
return
|
|
47329
|
+
return join5(home(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
46981
47330
|
}
|
|
46982
47331
|
function locateTrayExecutable() {
|
|
46983
47332
|
const candidates = [
|
|
46984
|
-
|
|
47333
|
+
join5(home(), "Applications", "ModelstatTray.app", "Contents", "MacOS", "modelstat-tray"),
|
|
46985
47334
|
"/Applications/ModelstatTray.app/Contents/MacOS/modelstat-tray"
|
|
46986
47335
|
];
|
|
46987
47336
|
for (const p of candidates) {
|
|
46988
|
-
if (
|
|
47337
|
+
if (existsSync6(p)) return p;
|
|
46989
47338
|
}
|
|
46990
47339
|
return null;
|
|
46991
47340
|
}
|
|
46992
47341
|
function writePlist(cliPath) {
|
|
46993
47342
|
const p = plistPath();
|
|
46994
|
-
mkdirSync2(
|
|
47343
|
+
mkdirSync2(dirname5(p), { recursive: true });
|
|
46995
47344
|
const tray = locateTrayExecutable();
|
|
46996
47345
|
const programArgs = tray ? ` <string>${tray}</string>` : [
|
|
46997
47346
|
` <string>${nodeBinary()}</string>`,
|
|
@@ -47011,8 +47360,8 @@ ${programArgs}
|
|
|
47011
47360
|
<key>KeepAlive</key>
|
|
47012
47361
|
<dict><key>SuccessfulExit</key><false/></dict>
|
|
47013
47362
|
<key>ThrottleInterval</key><integer>30</integer>
|
|
47014
|
-
<key>StandardOutPath</key><string>${
|
|
47015
|
-
<key>StandardErrorPath</key><string>${
|
|
47363
|
+
<key>StandardOutPath</key><string>${join5(logDir(), "out.log")}</string>
|
|
47364
|
+
<key>StandardErrorPath</key><string>${join5(logDir(), "err.log")}</string>
|
|
47016
47365
|
<key>EnvironmentVariables</key>
|
|
47017
47366
|
<dict>
|
|
47018
47367
|
<key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
@@ -47052,7 +47401,7 @@ function macUninstall() {
|
|
|
47052
47401
|
const target = `gui/${uid}/${SERVICE_LABEL}`;
|
|
47053
47402
|
launchctl(["bootout", target]);
|
|
47054
47403
|
const plist = plistPath();
|
|
47055
|
-
if (
|
|
47404
|
+
if (existsSync6(plist)) {
|
|
47056
47405
|
try {
|
|
47057
47406
|
unlinkSync(plist);
|
|
47058
47407
|
} catch {
|
|
@@ -47065,12 +47414,12 @@ function macStatus() {
|
|
|
47065
47414
|
return { running: r.ok, hint: r.ok ? "launchd managed" : "not installed" };
|
|
47066
47415
|
}
|
|
47067
47416
|
function systemdUnitPath() {
|
|
47068
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
47069
|
-
return
|
|
47417
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? join5(home(), ".config");
|
|
47418
|
+
return join5(xdg, "systemd", "user", `${SYSTEMD_UNIT}.service`);
|
|
47070
47419
|
}
|
|
47071
47420
|
function writeSystemdUnit(cliPath) {
|
|
47072
47421
|
const unitPath = systemdUnitPath();
|
|
47073
|
-
mkdirSync2(
|
|
47422
|
+
mkdirSync2(dirname5(unitPath), { recursive: true });
|
|
47074
47423
|
const unit = `[Unit]
|
|
47075
47424
|
Description=modelstat agent
|
|
47076
47425
|
Documentation=https://modelstat.ai
|
|
@@ -47085,8 +47434,8 @@ RestartSec=10
|
|
|
47085
47434
|
# Don't restart-storm if the service is persistently unreachable.
|
|
47086
47435
|
StartLimitIntervalSec=300
|
|
47087
47436
|
StartLimitBurst=10
|
|
47088
|
-
StandardOutput=append:${
|
|
47089
|
-
StandardError=append:${
|
|
47437
|
+
StandardOutput=append:${join5(logDir(), "out.log")}
|
|
47438
|
+
StandardError=append:${join5(logDir(), "err.log")}
|
|
47090
47439
|
|
|
47091
47440
|
[Install]
|
|
47092
47441
|
WantedBy=default.target
|
|
@@ -47111,7 +47460,7 @@ function linuxInstall() {
|
|
|
47111
47460
|
function linuxUninstall() {
|
|
47112
47461
|
systemctl(["disable", "--now", `${SYSTEMD_UNIT}.service`]);
|
|
47113
47462
|
const unit = systemdUnitPath();
|
|
47114
|
-
if (
|
|
47463
|
+
if (existsSync6(unit)) {
|
|
47115
47464
|
try {
|
|
47116
47465
|
unlinkSync(unit);
|
|
47117
47466
|
} catch {
|
|
@@ -47155,9 +47504,9 @@ function logsDir() {
|
|
|
47155
47504
|
}
|
|
47156
47505
|
function installTrayApp(sourceAppPath) {
|
|
47157
47506
|
if (platform2() !== "darwin") return null;
|
|
47158
|
-
if (!
|
|
47159
|
-
const dest =
|
|
47160
|
-
mkdirSync2(
|
|
47507
|
+
if (!existsSync6(sourceAppPath)) return null;
|
|
47508
|
+
const dest = join5(home(), "Applications", "ModelstatTray.app");
|
|
47509
|
+
mkdirSync2(dirname5(dest), { recursive: true });
|
|
47161
47510
|
spawnSync("rm", ["-rf", dest]);
|
|
47162
47511
|
const r = spawnSync("cp", ["-R", sourceAppPath, dest], { encoding: "utf8" });
|
|
47163
47512
|
if (r.status !== 0) {
|
|
@@ -47167,28 +47516,28 @@ function installTrayApp(sourceAppPath) {
|
|
|
47167
47516
|
}
|
|
47168
47517
|
function bundledTrayAppPath() {
|
|
47169
47518
|
if (platform2() !== "darwin") return null;
|
|
47170
|
-
const here2 =
|
|
47519
|
+
const here2 = dirname5(fileURLToPath2(import.meta.url));
|
|
47171
47520
|
const candidates = [
|
|
47172
47521
|
// Pre-built .app — CI with codesigning drops one here.
|
|
47173
|
-
|
|
47522
|
+
join5(here2, "..", "vendor", "ModelstatTray.app"),
|
|
47174
47523
|
// Local dev layout: apps/agent-dev/src/service.ts → ../../tray-mac/build/ModelstatTray.app
|
|
47175
|
-
|
|
47524
|
+
join5(here2, "..", "..", "tray-mac", "build", "ModelstatTray.app")
|
|
47176
47525
|
];
|
|
47177
47526
|
for (const c of candidates) {
|
|
47178
|
-
if (
|
|
47527
|
+
if (existsSync6(c)) return c;
|
|
47179
47528
|
}
|
|
47180
47529
|
const sourceDirs = [
|
|
47181
|
-
|
|
47182
|
-
|
|
47530
|
+
join5(here2, "..", "vendor", "tray-mac"),
|
|
47531
|
+
join5(here2, "..", "..", "tray-mac")
|
|
47183
47532
|
];
|
|
47184
47533
|
for (const src of sourceDirs) {
|
|
47185
|
-
const build =
|
|
47186
|
-
if (!
|
|
47534
|
+
const build = join5(src, "build-app.sh");
|
|
47535
|
+
if (!existsSync6(build)) continue;
|
|
47187
47536
|
if (!hasSwift()) return null;
|
|
47188
47537
|
const r = spawnSync("bash", [build], { cwd: src, encoding: "utf8" });
|
|
47189
47538
|
if (r.status === 0) {
|
|
47190
|
-
const app =
|
|
47191
|
-
if (
|
|
47539
|
+
const app = join5(src, "build", "ModelstatTray.app");
|
|
47540
|
+
if (existsSync6(app)) return app;
|
|
47192
47541
|
}
|
|
47193
47542
|
}
|
|
47194
47543
|
return null;
|
|
@@ -47420,12 +47769,33 @@ async function cmdConnect(opts) {
|
|
|
47420
47769
|
warn(`tray install skipped: ${e.message}`);
|
|
47421
47770
|
}
|
|
47422
47771
|
}
|
|
47772
|
+
step("Preparing local summariser (downloads on first run)");
|
|
47773
|
+
let modelReady = false;
|
|
47774
|
+
try {
|
|
47775
|
+
const { ensureLlamaModel: ensureLlamaModel2, defaultLlamaConfig: defaultLlamaConfig2 } = await Promise.resolve().then(() => (init_node2(), node_exports));
|
|
47776
|
+
await ensureLlamaModel2(defaultLlamaConfig2());
|
|
47777
|
+
modelReady = true;
|
|
47778
|
+
emitEvent(opts, "summariser_model_ready", {});
|
|
47779
|
+
ok("summariser model on disk");
|
|
47780
|
+
} catch (e) {
|
|
47781
|
+
emitEvent(opts, "summariser_model_failed", {
|
|
47782
|
+
error: e.message
|
|
47783
|
+
});
|
|
47784
|
+
warn(`couldn't prepare summariser model: ${e.message}`);
|
|
47785
|
+
warn(
|
|
47786
|
+
"the background service will retry the download on its first scan"
|
|
47787
|
+
);
|
|
47788
|
+
}
|
|
47423
47789
|
step("Installing background service so the agent survives reboots");
|
|
47424
47790
|
let serviceOk = false;
|
|
47425
47791
|
try {
|
|
47426
47792
|
const svc = installService();
|
|
47427
47793
|
serviceOk = true;
|
|
47428
|
-
emitEvent(opts, "service_installed", {
|
|
47794
|
+
emitEvent(opts, "service_installed", {
|
|
47795
|
+
path: svc.path,
|
|
47796
|
+
logs: svc.logs,
|
|
47797
|
+
summariser_ready: modelReady
|
|
47798
|
+
});
|
|
47429
47799
|
ok(`${platform4() === "darwin" ? "launchd" : "systemd --user"}: ${svc.path}`);
|
|
47430
47800
|
} catch (e) {
|
|
47431
47801
|
emitEvent(opts, "service_install_failed", { error: e.message });
|
|
@@ -47494,6 +47864,9 @@ async function cmdDiscover() {
|
|
|
47494
47864
|
console.log("\u2713 reported to backend");
|
|
47495
47865
|
}
|
|
47496
47866
|
async function cmdScan() {
|
|
47867
|
+
const { preflightSummariser: preflightSummariser2 } = await Promise.resolve().then(() => (init_pipeline2(), pipeline_exports));
|
|
47868
|
+
const sample = await preflightSummariser2();
|
|
47869
|
+
console.log(`[modelstat] summariser preflight ok: "${sample}"`);
|
|
47497
47870
|
const r = await scanAll();
|
|
47498
47871
|
console.log();
|
|
47499
47872
|
console.log(
|