modelstat 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +483 -121
- package/dist/cli.mjs.map +1 -1
- package/package.json +22 -18
- package/scripts/postinstall.mjs +120 -0
- package/vendor/tray-mac/Sources/ModelstatTray/main.swift +24 -3
- package/vendor/tray-mac/build-app.sh +0 -0
- package/LICENSE +0 -87
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
{
|
|
3
|
+
const [__msMaj, __msMin] = process.versions.node.split('.').map(Number);
|
|
4
|
+
if (__msMaj < 20 || (__msMaj === 20 && __msMin < 18)) {
|
|
5
|
+
process.stderr.write(`modelstat requires Node \u2265 20.18 (you have ${process.version}).\n`);
|
|
6
|
+
process.stderr.write('Install Node 20+: https://nodejs.org\n');
|
|
7
|
+
process.stderr.write('Debian/Ubuntu: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs\n');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
2
11
|
import { createRequire as __modelstatCR } from "node:module";
|
|
3
12
|
const require = __modelstatCR(import.meta.url);
|
|
4
13
|
var __create = Object.create;
|
|
@@ -21122,8 +21131,8 @@ var require_snapshot_utils = __commonJS({
|
|
|
21122
21131
|
var require_snapshot_recorder = __commonJS({
|
|
21123
21132
|
"../../node_modules/.pnpm/undici@7.25.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
|
|
21124
21133
|
"use strict";
|
|
21125
|
-
var { writeFile, readFile, mkdir } = __require("fs/promises");
|
|
21126
|
-
var { dirname:
|
|
21134
|
+
var { writeFile, readFile, mkdir: mkdir2 } = __require("fs/promises");
|
|
21135
|
+
var { dirname: dirname8, resolve: resolve6 } = __require("path");
|
|
21127
21136
|
var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("timers");
|
|
21128
21137
|
var { InvalidArgumentError, UndiciError } = require_errors();
|
|
21129
21138
|
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
|
|
@@ -21354,7 +21363,7 @@ var require_snapshot_recorder = __commonJS({
|
|
|
21354
21363
|
throw new InvalidArgumentError("Snapshot path is required");
|
|
21355
21364
|
}
|
|
21356
21365
|
const resolvedPath = resolve6(path5);
|
|
21357
|
-
await
|
|
21366
|
+
await mkdir2(dirname8(resolvedPath), { recursive: true });
|
|
21358
21367
|
const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
|
|
21359
21368
|
hash,
|
|
21360
21369
|
snapshot
|
|
@@ -44231,14 +44240,15 @@ var init_queue = __esm({
|
|
|
44231
44240
|
});
|
|
44232
44241
|
|
|
44233
44242
|
// ../../packages/companion-core/src/pipeline/prompts.ts
|
|
44234
|
-
var OLLAMA_CHAT_MODEL, OLLAMA_EMBED_MODEL, SUMMARISER_SYSTEM_PROMPT, SUMMARISER_MAX_TOKENS, SUMMARISER_TEMPERATURE, QWEN_CHARS_PER_TOKEN;
|
|
44243
|
+
var OLLAMA_CHAT_MODEL, OLLAMA_EMBED_MODEL, SUMMARISER_SYSTEM_PROMPT, SUMMARISER_MAX_TOKENS, ABSTRACT_OUTPUT_MAX_CHARS, SUMMARISER_TEMPERATURE, QWEN_CHARS_PER_TOKEN;
|
|
44235
44244
|
var init_prompts = __esm({
|
|
44236
44245
|
"../../packages/companion-core/src/pipeline/prompts.ts"() {
|
|
44237
44246
|
"use strict";
|
|
44238
44247
|
OLLAMA_CHAT_MODEL = "qwen3.5:0.8b";
|
|
44239
44248
|
OLLAMA_EMBED_MODEL = "bge-small-en-v1.5";
|
|
44240
|
-
SUMMARISER_SYSTEM_PROMPT = "
|
|
44241
|
-
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;
|
|
44242
44252
|
SUMMARISER_TEMPERATURE = 0.2;
|
|
44243
44253
|
QWEN_CHARS_PER_TOKEN = 3.3;
|
|
44244
44254
|
}
|
|
@@ -44278,6 +44288,7 @@ async function buildForOneSession(sessionId, events, adapters2) {
|
|
|
44278
44288
|
const boundaries = [];
|
|
44279
44289
|
let runStart = 0;
|
|
44280
44290
|
let runStartMs = Date.parse(sorted[0].ts);
|
|
44291
|
+
let runChars = (sorted[0].content_excerpt ?? "").length;
|
|
44281
44292
|
for (let i = 1; i < sorted.length; i++) {
|
|
44282
44293
|
const prev2 = sorted[i - 1];
|
|
44283
44294
|
const cur = sorted[i];
|
|
@@ -44288,6 +44299,7 @@ async function buildForOneSession(sessionId, events, adapters2) {
|
|
|
44288
44299
|
if (gap >= SEGMENT_TIME_GAP_MS) split = true;
|
|
44289
44300
|
else if (turnsInRun >= SEGMENT_MAX_TURNS) split = true;
|
|
44290
44301
|
else if (runMs >= SEGMENT_MAX_DURATION_MS) split = true;
|
|
44302
|
+
else if (runChars >= SEGMENT_MAX_CONTENT_CHARS) split = true;
|
|
44291
44303
|
else if (turnEmbeddings[i - 1].length > 0 && turnEmbeddings[i].length > 0 && cosineDistance(turnEmbeddings[i - 1], turnEmbeddings[i]) > SEGMENT_TOPIC_THRESHOLD) {
|
|
44292
44304
|
split = true;
|
|
44293
44305
|
}
|
|
@@ -44295,6 +44307,9 @@ async function buildForOneSession(sessionId, events, adapters2) {
|
|
|
44295
44307
|
boundaries.push(i);
|
|
44296
44308
|
runStart = i;
|
|
44297
44309
|
runStartMs = Date.parse(cur.ts);
|
|
44310
|
+
runChars = (cur.content_excerpt ?? "").length;
|
|
44311
|
+
} else {
|
|
44312
|
+
runChars += (cur.content_excerpt ?? "").length;
|
|
44298
44313
|
}
|
|
44299
44314
|
}
|
|
44300
44315
|
boundaries.push(sorted.length);
|
|
@@ -44355,13 +44370,16 @@ async function summariseSlice(sessionId, slice, adapters2) {
|
|
|
44355
44370
|
Sampled excerpts from the conversation (already redacted of PII and secrets):
|
|
44356
44371
|
${excerptBlock}
|
|
44357
44372
|
|
|
44358
|
-
Write
|
|
44359
|
-
Write
|
|
44360
|
-
|
|
44361
|
-
|
|
44362
|
-
|
|
44363
|
-
}
|
|
44364
|
-
|
|
44373
|
+
Write the SHORTEST keyword-dense paragraph (1-3 sentences, \u2264${ABSTRACT_OUTPUT_MAX_CHARS} chars) naming exactly what was achieved. Lead with an outcome verb. Pack with concrete domain keywords (frameworks, features, components, decisions). Skip narration and filler.` : `Session context: ${promptFacts || "generic coding session"}.
|
|
44374
|
+
Write the shortest possible paragraph naming the project + tool + visible action. Skip filler.`;
|
|
44375
|
+
const rawAbstract = await adapters2.summarize({
|
|
44376
|
+
prompt,
|
|
44377
|
+
maxTokens: SUMMARISER_MAX_TOKENS
|
|
44378
|
+
});
|
|
44379
|
+
if (!rawAbstract || rawAbstract.trim().length === 0) {
|
|
44380
|
+
throw new Error(
|
|
44381
|
+
`summariser returned empty abstract for session ${sessionId} (${slice.length} turns) \u2014 check that the configured summariser is healthy`
|
|
44382
|
+
);
|
|
44365
44383
|
}
|
|
44366
44384
|
const regexPass = redact(rawAbstract);
|
|
44367
44385
|
let abstractText = regexPass.text;
|
|
@@ -44414,7 +44432,11 @@ Write one sentence describing what the human was doing.`;
|
|
|
44414
44432
|
tool: first.tool,
|
|
44415
44433
|
started_at: first.ts,
|
|
44416
44434
|
ended_at: last.ts,
|
|
44417
|
-
|
|
44435
|
+
// Slice to the user-visible cap (ABSTRACT_OUTPUT_MAX_CHARS, default
|
|
44436
|
+
// 400) — well below the 512 storage cap. Models occasionally
|
|
44437
|
+
// overshoot the prompt's "≤N chars" instruction; the slice is the
|
|
44438
|
+
// hard guarantee the dashboard relies on.
|
|
44439
|
+
abstract: redacted.text.slice(0, ABSTRACT_OUTPUT_MAX_CHARS),
|
|
44418
44440
|
tokens,
|
|
44419
44441
|
tags,
|
|
44420
44442
|
// counts is `Record<string, number>` after the optional model
|
|
@@ -44478,18 +44500,20 @@ function inferEnvironment(branch) {
|
|
|
44478
44500
|
if (b === "dev" || b === "develop" || b.startsWith("dev/")) return "Dev";
|
|
44479
44501
|
return null;
|
|
44480
44502
|
}
|
|
44481
|
-
var SEGMENT_TIME_GAP_MS, SEGMENT_TOPIC_THRESHOLD, SEGMENT_MAX_TURNS, SEGMENT_MAX_DURATION_MS, ABSTRACT_MAX_CHARS;
|
|
44503
|
+
var SEGMENT_TIME_GAP_MS, SEGMENT_TOPIC_THRESHOLD, SEGMENT_MAX_TURNS, SEGMENT_MAX_DURATION_MS, SEGMENT_MAX_CONTENT_CHARS, ABSTRACT_MAX_CHARS;
|
|
44482
44504
|
var init_pipeline = __esm({
|
|
44483
44505
|
"../../packages/companion-core/src/pipeline/index.ts"() {
|
|
44484
44506
|
"use strict";
|
|
44485
44507
|
init_redact();
|
|
44486
44508
|
init_ids();
|
|
44509
|
+
init_prompts();
|
|
44487
44510
|
init_redact();
|
|
44488
44511
|
init_prompts();
|
|
44489
44512
|
SEGMENT_TIME_GAP_MS = 15 * 6e4;
|
|
44490
44513
|
SEGMENT_TOPIC_THRESHOLD = 0.35;
|
|
44491
44514
|
SEGMENT_MAX_TURNS = 100;
|
|
44492
44515
|
SEGMENT_MAX_DURATION_MS = 30 * 6e4;
|
|
44516
|
+
SEGMENT_MAX_CONTENT_CHARS = 12e3;
|
|
44493
44517
|
ABSTRACT_MAX_CHARS = 512;
|
|
44494
44518
|
}
|
|
44495
44519
|
});
|
|
@@ -44511,11 +44535,123 @@ var init_src3 = __esm({
|
|
|
44511
44535
|
// ../../packages/companion-core/src/node/file-queue-store.ts
|
|
44512
44536
|
import { promises as fs3 } from "fs";
|
|
44513
44537
|
import { dirname as dirname3 } from "path";
|
|
44514
|
-
var SENT_TTL_MS;
|
|
44538
|
+
var SENT_TTL_MS, FileQueueStore;
|
|
44515
44539
|
var init_file_queue_store = __esm({
|
|
44516
44540
|
"../../packages/companion-core/src/node/file-queue-store.ts"() {
|
|
44517
44541
|
"use strict";
|
|
44518
44542
|
SENT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
44543
|
+
FileQueueStore = class {
|
|
44544
|
+
filePath;
|
|
44545
|
+
items = /* @__PURE__ */ new Map();
|
|
44546
|
+
loaded = false;
|
|
44547
|
+
writing = null;
|
|
44548
|
+
constructor(filePath) {
|
|
44549
|
+
this.filePath = filePath;
|
|
44550
|
+
}
|
|
44551
|
+
/** Lazily load the snapshot on first access so construction stays
|
|
44552
|
+
* synchronous (matches the old SqliteQueueStore contract). */
|
|
44553
|
+
async ensureLoaded() {
|
|
44554
|
+
if (this.loaded) return;
|
|
44555
|
+
try {
|
|
44556
|
+
const raw = await fs3.readFile(this.filePath, "utf8");
|
|
44557
|
+
const parsed = JSON.parse(raw);
|
|
44558
|
+
if (parsed.version === 1 && parsed.items) {
|
|
44559
|
+
const now = Date.now();
|
|
44560
|
+
for (const [k, v] of Object.entries(parsed.items)) {
|
|
44561
|
+
if (v.synced && now - v.last_event_ts_ms > SENT_TTL_MS) continue;
|
|
44562
|
+
this.items.set(k, v);
|
|
44563
|
+
}
|
|
44564
|
+
}
|
|
44565
|
+
} catch (err) {
|
|
44566
|
+
if (err.code !== "ENOENT") {
|
|
44567
|
+
try {
|
|
44568
|
+
const bak = await fs3.readFile(`${this.filePath}.bak`, "utf8");
|
|
44569
|
+
const parsed = JSON.parse(bak);
|
|
44570
|
+
if (parsed.version === 1 && parsed.items) {
|
|
44571
|
+
for (const [k, v] of Object.entries(parsed.items)) {
|
|
44572
|
+
this.items.set(k, v);
|
|
44573
|
+
}
|
|
44574
|
+
}
|
|
44575
|
+
} catch {
|
|
44576
|
+
}
|
|
44577
|
+
}
|
|
44578
|
+
}
|
|
44579
|
+
this.loaded = true;
|
|
44580
|
+
}
|
|
44581
|
+
/** Serialize writes so overlapping mutations don't race on the
|
|
44582
|
+
* temp-file / rename swap. writing is a single-slot promise; each
|
|
44583
|
+
* caller awaits the previous write and then enqueues its own. */
|
|
44584
|
+
async persist() {
|
|
44585
|
+
const prior = this.writing;
|
|
44586
|
+
const run = async () => {
|
|
44587
|
+
await prior?.catch(() => void 0);
|
|
44588
|
+
const snap = {
|
|
44589
|
+
version: 1,
|
|
44590
|
+
items: Object.fromEntries(this.items.entries())
|
|
44591
|
+
};
|
|
44592
|
+
const tmp = `${this.filePath}.tmp`;
|
|
44593
|
+
await fs3.mkdir(dirname3(this.filePath), { recursive: true });
|
|
44594
|
+
await fs3.writeFile(tmp, JSON.stringify(snap), "utf8");
|
|
44595
|
+
try {
|
|
44596
|
+
await fs3.rename(this.filePath, `${this.filePath}.bak`);
|
|
44597
|
+
} catch (err) {
|
|
44598
|
+
if (err.code !== "ENOENT") throw err;
|
|
44599
|
+
}
|
|
44600
|
+
await fs3.rename(tmp, this.filePath);
|
|
44601
|
+
};
|
|
44602
|
+
this.writing = run();
|
|
44603
|
+
try {
|
|
44604
|
+
await this.writing;
|
|
44605
|
+
} finally {
|
|
44606
|
+
this.writing = null;
|
|
44607
|
+
}
|
|
44608
|
+
}
|
|
44609
|
+
async put(item) {
|
|
44610
|
+
await this.ensureLoaded();
|
|
44611
|
+
if (this.items.has(item.source_event_id)) return;
|
|
44612
|
+
this.items.set(item.source_event_id, item);
|
|
44613
|
+
await this.persist();
|
|
44614
|
+
}
|
|
44615
|
+
async has(sourceEventId2) {
|
|
44616
|
+
await this.ensureLoaded();
|
|
44617
|
+
return this.items.has(sourceEventId2);
|
|
44618
|
+
}
|
|
44619
|
+
async listUnsentBySession() {
|
|
44620
|
+
await this.ensureLoaded();
|
|
44621
|
+
const out = /* @__PURE__ */ new Map();
|
|
44622
|
+
const sorted = [...this.items.values()].filter((i) => !i.synced).sort((a, b) => {
|
|
44623
|
+
if (a.session_id !== b.session_id)
|
|
44624
|
+
return a.session_id < b.session_id ? -1 : 1;
|
|
44625
|
+
return a.last_event_ts_ms - b.last_event_ts_ms;
|
|
44626
|
+
});
|
|
44627
|
+
for (const it of sorted) {
|
|
44628
|
+
const arr = out.get(it.session_id) ?? [];
|
|
44629
|
+
arr.push(it);
|
|
44630
|
+
out.set(it.session_id, arr);
|
|
44631
|
+
}
|
|
44632
|
+
return out;
|
|
44633
|
+
}
|
|
44634
|
+
async markSent(sourceEventIds, batchId2) {
|
|
44635
|
+
if (sourceEventIds.length === 0) return;
|
|
44636
|
+
await this.ensureLoaded();
|
|
44637
|
+
for (const id of sourceEventIds) {
|
|
44638
|
+
const it = this.items.get(id);
|
|
44639
|
+
if (!it) continue;
|
|
44640
|
+
it.synced = true;
|
|
44641
|
+
it.sent_batch_id = batchId2;
|
|
44642
|
+
}
|
|
44643
|
+
await this.persist();
|
|
44644
|
+
}
|
|
44645
|
+
async countUnsent() {
|
|
44646
|
+
await this.ensureLoaded();
|
|
44647
|
+
let n = 0;
|
|
44648
|
+
for (const it of this.items.values()) if (!it.synced) n++;
|
|
44649
|
+
return n;
|
|
44650
|
+
}
|
|
44651
|
+
/** Compat stub — Sqlite store exposed close(); nothing to release here. */
|
|
44652
|
+
close() {
|
|
44653
|
+
}
|
|
44654
|
+
};
|
|
44519
44655
|
}
|
|
44520
44656
|
});
|
|
44521
44657
|
|
|
@@ -44581,17 +44717,192 @@ var init_ollama = __esm({
|
|
|
44581
44717
|
}
|
|
44582
44718
|
});
|
|
44583
44719
|
|
|
44720
|
+
// ../../packages/companion-core/src/node/llama.ts
|
|
44721
|
+
import { mkdir } from "fs/promises";
|
|
44722
|
+
import { existsSync as existsSync5 } from "fs";
|
|
44723
|
+
import { homedir as homedir4 } from "os";
|
|
44724
|
+
import { dirname as dirname4, join as join3 } from "path";
|
|
44725
|
+
function defaultLlamaConfig() {
|
|
44726
|
+
const env2 = globalThis.process?.env ?? {};
|
|
44727
|
+
const modelsDir = env2.MODELSTAT_MODELS_DIR ?? join3(homedir4(), ".modelstat", "models");
|
|
44728
|
+
const modelUrl = env2.MODELSTAT_LLAMA_MODEL_URL ?? DEFAULT_LLAMA_MODEL_URL;
|
|
44729
|
+
const modelPath = env2.MODELSTAT_LLAMA_MODEL_PATH ?? join3(modelsDir, basenameFromUrl(modelUrl));
|
|
44730
|
+
return {
|
|
44731
|
+
modelPath,
|
|
44732
|
+
modelUrl,
|
|
44733
|
+
modelsDir,
|
|
44734
|
+
// Qwen3.5-4B has a 128K native context; we only need enough to
|
|
44735
|
+
// fit our prompt (~2 KB) plus thinking budget plus answer.
|
|
44736
|
+
// 4096 is plenty and keeps memory footprint reasonable on
|
|
44737
|
+
// older machines.
|
|
44738
|
+
contextSize: Number(env2.MODELSTAT_LLAMA_CONTEXT ?? 4096)
|
|
44739
|
+
};
|
|
44740
|
+
}
|
|
44741
|
+
function stripThinking(text) {
|
|
44742
|
+
return text.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/<think>[\s\S]*$/i, "").trim();
|
|
44743
|
+
}
|
|
44744
|
+
function basenameFromUrl(url) {
|
|
44745
|
+
const clean = url.split("?")[0].split("#")[0];
|
|
44746
|
+
const parts = clean.split("/");
|
|
44747
|
+
return parts[parts.length - 1] || "model.gguf";
|
|
44748
|
+
}
|
|
44749
|
+
async function ensureLlamaModel(cfg = defaultLlamaConfig()) {
|
|
44750
|
+
if (existsSync5(cfg.modelPath)) return cfg.modelPath;
|
|
44751
|
+
await mkdir(dirname4(cfg.modelPath), { recursive: true });
|
|
44752
|
+
const res = await fetch(cfg.modelUrl);
|
|
44753
|
+
if (!res.ok || !res.body) {
|
|
44754
|
+
throw new Error(
|
|
44755
|
+
`model download failed: ${res.status} ${res.statusText} (${cfg.modelUrl})`
|
|
44756
|
+
);
|
|
44757
|
+
}
|
|
44758
|
+
const total = Number(res.headers.get("content-length") ?? 0);
|
|
44759
|
+
const totalMb = total > 0 ? (total / (1024 * 1024)).toFixed(0) : "?";
|
|
44760
|
+
console.log(
|
|
44761
|
+
`[modelstat] downloading summariser model (~${totalMb} MB) \u2192 ${cfg.modelPath}`
|
|
44762
|
+
);
|
|
44763
|
+
const tmp = `${cfg.modelPath}.partial`;
|
|
44764
|
+
const { createWriteStream: createWriteStream2 } = await import("fs");
|
|
44765
|
+
const { Readable: Readable3 } = await import("stream");
|
|
44766
|
+
const { rename } = await import("fs/promises");
|
|
44767
|
+
const out = createWriteStream2(tmp);
|
|
44768
|
+
const isTty = Boolean(
|
|
44769
|
+
process.stdout.isTTY
|
|
44770
|
+
);
|
|
44771
|
+
let received = 0;
|
|
44772
|
+
let lastLog = 0;
|
|
44773
|
+
let lastBytes = 0;
|
|
44774
|
+
let lastTimeForRate = Date.now();
|
|
44775
|
+
const startTime = Date.now();
|
|
44776
|
+
const renderProgress = (final = false) => {
|
|
44777
|
+
const now = Date.now();
|
|
44778
|
+
const dt = (now - lastTimeForRate) / 1e3;
|
|
44779
|
+
const dBytes = received - lastBytes;
|
|
44780
|
+
const rateMbps = dt > 0 ? dBytes / 1024 / 1024 / dt : 0;
|
|
44781
|
+
lastBytes = received;
|
|
44782
|
+
lastTimeForRate = now;
|
|
44783
|
+
const mb = (received / (1024 * 1024)).toFixed(1);
|
|
44784
|
+
const pct = total > 0 ? (received / total * 100).toFixed(1) : "?";
|
|
44785
|
+
const eta = total > 0 && rateMbps > 0 ? Math.max(0, Math.round((total - received) / 1024 / 1024 / rateMbps)) : null;
|
|
44786
|
+
const etaStr = eta != null ? ` \xB7 ETA ${eta}s` : "";
|
|
44787
|
+
const elapsed = ((now - startTime) / 1e3).toFixed(0);
|
|
44788
|
+
const line = `[modelstat] ${mb} / ${totalMb} MB (${pct}%) \xB7 ${rateMbps.toFixed(1)} MB/s${etaStr} \xB7 ${elapsed}s`;
|
|
44789
|
+
if (isTty && !final) {
|
|
44790
|
+
process.stdout.write(`\r${line}\x1B[K`);
|
|
44791
|
+
} else {
|
|
44792
|
+
console.log(line);
|
|
44793
|
+
}
|
|
44794
|
+
};
|
|
44795
|
+
const nodeStream = Readable3.fromWeb(
|
|
44796
|
+
res.body
|
|
44797
|
+
);
|
|
44798
|
+
nodeStream.on("data", (chunk) => {
|
|
44799
|
+
received += chunk.length;
|
|
44800
|
+
const now = Date.now();
|
|
44801
|
+
if (now - lastLog > (isTty ? 200 : 2e3)) {
|
|
44802
|
+
renderProgress();
|
|
44803
|
+
lastLog = now;
|
|
44804
|
+
}
|
|
44805
|
+
});
|
|
44806
|
+
await new Promise((resolve6, reject) => {
|
|
44807
|
+
nodeStream.pipe(out);
|
|
44808
|
+
out.on("finish", () => resolve6());
|
|
44809
|
+
out.on("error", reject);
|
|
44810
|
+
nodeStream.on("error", reject);
|
|
44811
|
+
});
|
|
44812
|
+
renderProgress(true);
|
|
44813
|
+
if (isTty) process.stdout.write("\n");
|
|
44814
|
+
await rename(tmp, cfg.modelPath);
|
|
44815
|
+
console.log(`[modelstat] summariser model ready (${cfg.modelPath})`);
|
|
44816
|
+
return cfg.modelPath;
|
|
44817
|
+
}
|
|
44818
|
+
async function loadOnce(cfg) {
|
|
44819
|
+
if (loaded) return loaded;
|
|
44820
|
+
if (loadPromise) return loadPromise;
|
|
44821
|
+
loadPromise = (async () => {
|
|
44822
|
+
const llamaMod = await import("node-llama-cpp");
|
|
44823
|
+
const modelPath = await ensureLlamaModel(cfg);
|
|
44824
|
+
const llama = await llamaMod.getLlama();
|
|
44825
|
+
const model = await llama.loadModel({ modelPath });
|
|
44826
|
+
const context = await model.createContext({ contextSize: cfg.contextSize });
|
|
44827
|
+
const session = new llamaMod.LlamaChatSession({
|
|
44828
|
+
contextSequence: context.getSequence(),
|
|
44829
|
+
systemPrompt: SUMMARISER_SYSTEM_PROMPT
|
|
44830
|
+
});
|
|
44831
|
+
loaded = { session };
|
|
44832
|
+
return loaded;
|
|
44833
|
+
})();
|
|
44834
|
+
try {
|
|
44835
|
+
return await loadPromise;
|
|
44836
|
+
} catch (err) {
|
|
44837
|
+
loadPromise = null;
|
|
44838
|
+
throw err;
|
|
44839
|
+
}
|
|
44840
|
+
}
|
|
44841
|
+
function llamaSummarize(cfg = defaultLlamaConfig()) {
|
|
44842
|
+
return async ({ prompt, maxTokens }) => {
|
|
44843
|
+
const { session } = await loadOnce(cfg);
|
|
44844
|
+
const run = inflight.then(async () => {
|
|
44845
|
+
session.resetChatHistory();
|
|
44846
|
+
void maxTokens;
|
|
44847
|
+
const raw = await session.prompt(prompt, {
|
|
44848
|
+
temperature: SUMMARISER_TEMPERATURE,
|
|
44849
|
+
maxTokens: LLAMA_MAX_TOKENS
|
|
44850
|
+
});
|
|
44851
|
+
const stripped = stripThinking(raw ?? "");
|
|
44852
|
+
if (stripped.length === 0) {
|
|
44853
|
+
throw new Error(
|
|
44854
|
+
`bundled summariser produced no answer text after stripping <think> blocks (raw length=${(raw ?? "").length}). The thinking budget may be too low or the model template is misconfigured.`
|
|
44855
|
+
);
|
|
44856
|
+
}
|
|
44857
|
+
return stripped.slice(0, 240);
|
|
44858
|
+
});
|
|
44859
|
+
inflight = run.catch(() => void 0);
|
|
44860
|
+
return run;
|
|
44861
|
+
};
|
|
44862
|
+
}
|
|
44863
|
+
var DEFAULT_LLAMA_MODEL_URL, LLAMA_MAX_TOKENS, loaded, loadPromise, inflight;
|
|
44864
|
+
var init_llama = __esm({
|
|
44865
|
+
"../../packages/companion-core/src/node/llama.ts"() {
|
|
44866
|
+
"use strict";
|
|
44867
|
+
init_prompts();
|
|
44868
|
+
DEFAULT_LLAMA_MODEL_URL = "https://huggingface.co/lmstudio-community/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf";
|
|
44869
|
+
LLAMA_MAX_TOKENS = 1024;
|
|
44870
|
+
loaded = null;
|
|
44871
|
+
loadPromise = null;
|
|
44872
|
+
inflight = Promise.resolve();
|
|
44873
|
+
}
|
|
44874
|
+
});
|
|
44875
|
+
|
|
44584
44876
|
// ../../packages/companion-core/src/node/index.ts
|
|
44877
|
+
var node_exports = {};
|
|
44878
|
+
__export(node_exports, {
|
|
44879
|
+
DEFAULT_LLAMA_MODEL_URL: () => DEFAULT_LLAMA_MODEL_URL,
|
|
44880
|
+
FileQueueStore: () => FileQueueStore,
|
|
44881
|
+
SqliteQueueStore: () => FileQueueStore,
|
|
44882
|
+
defaultLlamaConfig: () => defaultLlamaConfig,
|
|
44883
|
+
defaultOllamaConfig: () => defaultOllamaConfig,
|
|
44884
|
+
ensureLlamaModel: () => ensureLlamaModel,
|
|
44885
|
+
llamaSummarize: () => llamaSummarize,
|
|
44886
|
+
ollamaEmbed: () => ollamaEmbed,
|
|
44887
|
+
ollamaSummarize: () => ollamaSummarize,
|
|
44888
|
+
ollamaTokenize: () => ollamaTokenize
|
|
44889
|
+
});
|
|
44585
44890
|
var init_node2 = __esm({
|
|
44586
44891
|
"../../packages/companion-core/src/node/index.ts"() {
|
|
44587
44892
|
"use strict";
|
|
44588
44893
|
init_file_queue_store();
|
|
44589
44894
|
init_file_queue_store();
|
|
44590
44895
|
init_ollama();
|
|
44896
|
+
init_llama();
|
|
44591
44897
|
}
|
|
44592
44898
|
});
|
|
44593
44899
|
|
|
44594
44900
|
// src/pipeline.ts
|
|
44901
|
+
var pipeline_exports = {};
|
|
44902
|
+
__export(pipeline_exports, {
|
|
44903
|
+
buildSegments: () => buildSegments,
|
|
44904
|
+
preflightSummariser: () => preflightSummariser
|
|
44905
|
+
});
|
|
44595
44906
|
async function probeOllama(baseUrl) {
|
|
44596
44907
|
try {
|
|
44597
44908
|
const ctrl = new AbortController();
|
|
@@ -44606,40 +44917,58 @@ async function probeOllama(baseUrl) {
|
|
|
44606
44917
|
return false;
|
|
44607
44918
|
}
|
|
44608
44919
|
}
|
|
44609
|
-
function
|
|
44920
|
+
function bundledAdapters() {
|
|
44610
44921
|
return {
|
|
44611
44922
|
embed: async () => [],
|
|
44612
|
-
|
|
44613
|
-
// the `promptFacts` string which is what we want here.
|
|
44614
|
-
summarize: async () => {
|
|
44615
|
-
throw new Error("ollama_unavailable");
|
|
44616
|
-
},
|
|
44923
|
+
summarize: llamaSummarize(defaultLlamaConfig()),
|
|
44617
44924
|
tokenize: (text) => Math.max(1, Math.ceil(text.length / 4))
|
|
44618
44925
|
};
|
|
44619
44926
|
}
|
|
44620
44927
|
async function getAdapters() {
|
|
44621
44928
|
if (adapters && probed) return adapters;
|
|
44622
|
-
const
|
|
44623
|
-
const
|
|
44929
|
+
const ollamaCfg = defaultOllamaConfig();
|
|
44930
|
+
const ollamaUp = await probeOllama(ollamaCfg.baseUrl);
|
|
44624
44931
|
probed = true;
|
|
44625
|
-
if (
|
|
44626
|
-
console.log(
|
|
44932
|
+
if (ollamaUp) {
|
|
44933
|
+
console.log(
|
|
44934
|
+
`[modelstat] ollama up at ${ollamaCfg.baseUrl} \u2014 using ${ollamaCfg.chatModel} for summarisation`
|
|
44935
|
+
);
|
|
44627
44936
|
adapters = {
|
|
44628
|
-
embed: ollamaEmbed(
|
|
44629
|
-
summarize: ollamaSummarize(
|
|
44937
|
+
embed: ollamaEmbed(ollamaCfg),
|
|
44938
|
+
summarize: ollamaSummarize(ollamaCfg),
|
|
44630
44939
|
tokenize: ollamaTokenize()
|
|
44631
44940
|
};
|
|
44632
|
-
|
|
44633
|
-
|
|
44634
|
-
|
|
44941
|
+
return adapters;
|
|
44942
|
+
}
|
|
44943
|
+
try {
|
|
44944
|
+
await import("node-llama-cpp");
|
|
44945
|
+
} catch (err) {
|
|
44946
|
+
throw new Error(
|
|
44947
|
+
`modelstat agent can't start: ollama is not reachable AND the bundled summariser (node-llama-cpp) failed to load. Either start ollama (\`ollama serve\`) with \`ollama pull ${ollamaCfg.chatModel}\`, or reinstall the agent so the native binding is rebuilt for this platform. Underlying error: ${err.message}`
|
|
44635
44948
|
);
|
|
44636
|
-
adapters = fallbackAdapters();
|
|
44637
44949
|
}
|
|
44950
|
+
console.log(
|
|
44951
|
+
"[modelstat] using bundled local summariser (Qwen3.5-4B, runs on this machine)"
|
|
44952
|
+
);
|
|
44953
|
+
adapters = bundledAdapters();
|
|
44638
44954
|
return adapters;
|
|
44639
44955
|
}
|
|
44640
44956
|
async function buildSegments(events) {
|
|
44641
44957
|
return buildSegmentsForSession(events, await getAdapters());
|
|
44642
44958
|
}
|
|
44959
|
+
async function preflightSummariser() {
|
|
44960
|
+
const a = await getAdapters();
|
|
44961
|
+
const out = await a.summarize({
|
|
44962
|
+
prompt: 'Session context: smoke test. Sampled excerpts:\n [turn 1] "hello world"\nWrite ONE sentence (\u2264240 chars) describing what the human was doing.',
|
|
44963
|
+
maxTokens: 32
|
|
44964
|
+
});
|
|
44965
|
+
if (!out || out.trim().length === 0) {
|
|
44966
|
+
throw new Error(
|
|
44967
|
+
"summariser preflight returned empty output \u2014 the configured summariser is reachable but produced no text. Check the model is loaded."
|
|
44968
|
+
);
|
|
44969
|
+
}
|
|
44970
|
+
return out.length > 60 ? `${out.slice(0, 57)}\u2026` : out;
|
|
44971
|
+
}
|
|
44643
44972
|
var adapters, probed;
|
|
44644
44973
|
var init_pipeline2 = __esm({
|
|
44645
44974
|
"src/pipeline.ts"() {
|
|
@@ -44653,23 +44982,23 @@ var init_pipeline2 = __esm({
|
|
|
44653
44982
|
|
|
44654
44983
|
// src/scan.ts
|
|
44655
44984
|
import { readdir, stat as stat2 } from "fs/promises";
|
|
44656
|
-
import { homedir as
|
|
44657
|
-
import { join as
|
|
44985
|
+
import { homedir as homedir5 } from "os";
|
|
44986
|
+
import { join as join4 } from "path";
|
|
44658
44987
|
async function scanAll(cb = {}) {
|
|
44659
44988
|
const deviceId = state.deviceId;
|
|
44660
44989
|
if (!deviceId) throw new Error("agent not enrolled \u2014 run `register` first");
|
|
44661
44990
|
const jobs = [];
|
|
44662
44991
|
try {
|
|
44663
|
-
const base =
|
|
44992
|
+
const base = join4(homedir5(), ".claude/projects");
|
|
44664
44993
|
const projects = await readdir(base).catch(() => []);
|
|
44665
44994
|
for (const p of projects) {
|
|
44666
|
-
const dir =
|
|
44995
|
+
const dir = join4(base, p);
|
|
44667
44996
|
const ds = await stat2(dir).catch(() => null);
|
|
44668
44997
|
if (!ds?.isDirectory()) continue;
|
|
44669
44998
|
const files = await readdir(dir);
|
|
44670
44999
|
for (const f of files) {
|
|
44671
45000
|
if (!f.endsWith(".jsonl")) continue;
|
|
44672
|
-
const full =
|
|
45001
|
+
const full = join4(dir, f);
|
|
44673
45002
|
jobs.push({
|
|
44674
45003
|
path: full,
|
|
44675
45004
|
parse: async () => {
|
|
@@ -44683,17 +45012,17 @@ async function scanAll(cb = {}) {
|
|
|
44683
45012
|
console.warn("claude scan skipped:", e.message);
|
|
44684
45013
|
}
|
|
44685
45014
|
try {
|
|
44686
|
-
const base =
|
|
45015
|
+
const base = join4(homedir5(), ".codex/sessions");
|
|
44687
45016
|
const years = await readdir(base).catch(() => []);
|
|
44688
45017
|
for (const y of years) {
|
|
44689
|
-
const months = await readdir(
|
|
45018
|
+
const months = await readdir(join4(base, y)).catch(() => []);
|
|
44690
45019
|
for (const m of months) {
|
|
44691
|
-
const days = await readdir(
|
|
45020
|
+
const days = await readdir(join4(base, y, m)).catch(() => []);
|
|
44692
45021
|
for (const d of days) {
|
|
44693
|
-
const files = await readdir(
|
|
45022
|
+
const files = await readdir(join4(base, y, m, d)).catch(() => []);
|
|
44694
45023
|
for (const f of files) {
|
|
44695
45024
|
if (!f.startsWith("rollout-") || !f.endsWith(".jsonl")) continue;
|
|
44696
|
-
const full =
|
|
45025
|
+
const full = join4(base, y, m, d, f);
|
|
44697
45026
|
jobs.push({
|
|
44698
45027
|
path: full,
|
|
44699
45028
|
parse: async () => {
|
|
@@ -44775,7 +45104,7 @@ var init_scan = __esm({
|
|
|
44775
45104
|
// src/lock.ts
|
|
44776
45105
|
import {
|
|
44777
45106
|
closeSync,
|
|
44778
|
-
existsSync as
|
|
45107
|
+
existsSync as existsSync7,
|
|
44779
45108
|
mkdirSync as mkdirSync3,
|
|
44780
45109
|
openSync,
|
|
44781
45110
|
readFileSync as readFileSync3,
|
|
@@ -44784,8 +45113,8 @@ import {
|
|
|
44784
45113
|
writeFileSync as writeFileSync4,
|
|
44785
45114
|
writeSync
|
|
44786
45115
|
} from "fs";
|
|
44787
|
-
import { homedir as
|
|
44788
|
-
import { join as
|
|
45116
|
+
import { homedir as homedir7 } from "os";
|
|
45117
|
+
import { join as join6 } from "path";
|
|
44789
45118
|
function isProcessAlive(pid) {
|
|
44790
45119
|
if (!pid || pid <= 0) return false;
|
|
44791
45120
|
try {
|
|
@@ -44886,8 +45215,8 @@ var LOCK_DIR, LOCK_FILE;
|
|
|
44886
45215
|
var init_lock = __esm({
|
|
44887
45216
|
"src/lock.ts"() {
|
|
44888
45217
|
"use strict";
|
|
44889
|
-
LOCK_DIR =
|
|
44890
|
-
LOCK_FILE =
|
|
45218
|
+
LOCK_DIR = join6(homedir7(), ".modelstat");
|
|
45219
|
+
LOCK_FILE = join6(LOCK_DIR, "daemon.lock");
|
|
44891
45220
|
}
|
|
44892
45221
|
});
|
|
44893
45222
|
|
|
@@ -45624,9 +45953,9 @@ var init_handler = __esm({
|
|
|
45624
45953
|
if (this.fsw.closed) {
|
|
45625
45954
|
return;
|
|
45626
45955
|
}
|
|
45627
|
-
const
|
|
45956
|
+
const dirname8 = sysPath.dirname(file);
|
|
45628
45957
|
const basename4 = sysPath.basename(file);
|
|
45629
|
-
const parent = this.fsw._getWatchedDir(
|
|
45958
|
+
const parent = this.fsw._getWatchedDir(dirname8);
|
|
45630
45959
|
let prevStats = stats;
|
|
45631
45960
|
if (parent.has(basename4))
|
|
45632
45961
|
return;
|
|
@@ -45653,7 +45982,7 @@ var init_handler = __esm({
|
|
|
45653
45982
|
prevStats = newStats2;
|
|
45654
45983
|
}
|
|
45655
45984
|
} catch (error) {
|
|
45656
|
-
this.fsw._remove(
|
|
45985
|
+
this.fsw._remove(dirname8, basename4);
|
|
45657
45986
|
}
|
|
45658
45987
|
} else if (parent.has(basename4)) {
|
|
45659
45988
|
const at = newStats.atimeMs;
|
|
@@ -46617,7 +46946,7 @@ __export(daemon_exports, {
|
|
|
46617
46946
|
setProgress: () => setProgress,
|
|
46618
46947
|
setQueue: () => setQueue
|
|
46619
46948
|
});
|
|
46620
|
-
import { existsSync as
|
|
46949
|
+
import { existsSync as existsSync8, statSync as statSync2 } from "fs";
|
|
46621
46950
|
function setPhase(phase, message) {
|
|
46622
46951
|
status.phase = phase;
|
|
46623
46952
|
status.message = message ?? null;
|
|
@@ -46736,24 +47065,33 @@ async function runDaemon(opts = {}) {
|
|
|
46736
47065
|
const hb = setInterval(() => void sendHeartbeat(), HEARTBEAT_INTERVAL_MS);
|
|
46737
47066
|
hb.unref();
|
|
46738
47067
|
void sendHeartbeat();
|
|
47068
|
+
try {
|
|
47069
|
+
setPhase("starting", "Preflight: summariser");
|
|
47070
|
+
const { preflightSummariser: preflightSummariser2 } = await Promise.resolve().then(() => (init_pipeline2(), pipeline_exports));
|
|
47071
|
+
const sample = await preflightSummariser2();
|
|
47072
|
+
console.log(`[modelstat] summariser preflight ok: "${sample}"`);
|
|
47073
|
+
} catch (err) {
|
|
47074
|
+
setPhase("error", `summariser preflight failed: ${err.message}`);
|
|
47075
|
+
throw err;
|
|
47076
|
+
}
|
|
46739
47077
|
await runDiscovery();
|
|
46740
47078
|
await runScanCycle("startup");
|
|
46741
47079
|
const chokidar = (await Promise.resolve().then(() => (init_esm2(), esm_exports))).default;
|
|
46742
|
-
const { homedir:
|
|
46743
|
-
const { join:
|
|
46744
|
-
const home2 =
|
|
47080
|
+
const { homedir: homedir9, platform: platform5 } = await import("os");
|
|
47081
|
+
const { join: join10 } = await import("path");
|
|
47082
|
+
const home2 = homedir9();
|
|
46745
47083
|
const dirs = [
|
|
46746
|
-
|
|
46747
|
-
|
|
46748
|
-
|
|
46749
|
-
|
|
47084
|
+
join10(home2, ".claude/projects"),
|
|
47085
|
+
join10(home2, ".codex/sessions"),
|
|
47086
|
+
join10(home2, ".cursor/ai-tracking"),
|
|
47087
|
+
join10(home2, ".gemini"),
|
|
46750
47088
|
...platform5() === "darwin" ? [
|
|
46751
|
-
|
|
46752
|
-
|
|
47089
|
+
join10(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
|
|
47090
|
+
join10(home2, "Library/Application Support/Claude")
|
|
46753
47091
|
] : [
|
|
46754
|
-
|
|
47092
|
+
join10(home2, ".config/Cursor/User/workspaceStorage")
|
|
46755
47093
|
]
|
|
46756
|
-
].filter((p) =>
|
|
47094
|
+
].filter((p) => existsSync8(p) && statSync2(p).isDirectory());
|
|
46757
47095
|
setPhase("watching", `Watching ${dirs.length} directories`);
|
|
46758
47096
|
const watcher = chokidar.watch(dirs, {
|
|
46759
47097
|
persistent: true,
|
|
@@ -46820,37 +47158,37 @@ var watch_exports = {};
|
|
|
46820
47158
|
__export(watch_exports, {
|
|
46821
47159
|
watchForever: () => watchForever
|
|
46822
47160
|
});
|
|
46823
|
-
import { existsSync as
|
|
46824
|
-
import { homedir as
|
|
46825
|
-
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";
|
|
46826
47164
|
function resolveWatchDirs() {
|
|
46827
|
-
const home2 =
|
|
46828
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
46829
|
-
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");
|
|
46830
47168
|
const candidates = [
|
|
46831
47169
|
// universal (default HOME-rooted CLI data dirs)
|
|
46832
|
-
|
|
46833
|
-
|
|
46834
|
-
|
|
46835
|
-
|
|
46836
|
-
|
|
47170
|
+
join9(home2, ".claude/projects"),
|
|
47171
|
+
join9(home2, ".codex/sessions"),
|
|
47172
|
+
join9(home2, ".cursor/ai-tracking"),
|
|
47173
|
+
join9(home2, ".gemini"),
|
|
47174
|
+
join9(home2, ".aider"),
|
|
46837
47175
|
// XDG / Linux
|
|
46838
|
-
|
|
46839
|
-
|
|
46840
|
-
|
|
46841
|
-
|
|
46842
|
-
|
|
46843
|
-
|
|
47176
|
+
join9(xdgConfig, "claude/projects"),
|
|
47177
|
+
join9(xdgConfig, "codex/sessions"),
|
|
47178
|
+
join9(xdgConfig, "Cursor/User/workspaceStorage"),
|
|
47179
|
+
join9(xdgConfig, "Code/User/workspaceStorage"),
|
|
47180
|
+
join9(xdgConfig, "Code - Insiders/User/workspaceStorage"),
|
|
47181
|
+
join9(xdgData, "claude/projects"),
|
|
46844
47182
|
// macOS
|
|
46845
47183
|
...platform3() === "darwin" ? [
|
|
46846
|
-
|
|
46847
|
-
|
|
46848
|
-
|
|
46849
|
-
|
|
46850
|
-
|
|
47184
|
+
join9(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
|
|
47185
|
+
join9(home2, "Library/Application Support/Claude"),
|
|
47186
|
+
join9(home2, "Library/Application Support/Code/User/workspaceStorage"),
|
|
47187
|
+
join9(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
|
|
47188
|
+
join9(home2, "Library/Application Support/Zed")
|
|
46851
47189
|
] : []
|
|
46852
47190
|
];
|
|
46853
|
-
return Array.from(new Set(candidates)).filter((p) =>
|
|
47191
|
+
return Array.from(new Set(candidates)).filter((p) => existsSync9(p));
|
|
46854
47192
|
}
|
|
46855
47193
|
async function safeScan(reason) {
|
|
46856
47194
|
if (scanning) {
|
|
@@ -46923,30 +47261,30 @@ import { createInterface as createInterface3 } from "readline";
|
|
|
46923
47261
|
import { spawnSync } from "child_process";
|
|
46924
47262
|
import {
|
|
46925
47263
|
copyFileSync,
|
|
46926
|
-
existsSync as
|
|
47264
|
+
existsSync as existsSync6,
|
|
46927
47265
|
mkdirSync as mkdirSync2,
|
|
46928
47266
|
unlinkSync,
|
|
46929
47267
|
writeFileSync as writeFileSync3
|
|
46930
47268
|
} from "fs";
|
|
46931
|
-
import { homedir as
|
|
46932
|
-
import { dirname as
|
|
47269
|
+
import { homedir as homedir6, platform as platform2, userInfo } from "os";
|
|
47270
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
46933
47271
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
46934
47272
|
var SERVICE_LABEL = "ai.modelstat.agent";
|
|
46935
47273
|
var SYSTEMD_UNIT = "modelstat";
|
|
46936
47274
|
function home() {
|
|
46937
|
-
return
|
|
47275
|
+
return homedir6();
|
|
46938
47276
|
}
|
|
46939
47277
|
function stateDir() {
|
|
46940
|
-
return
|
|
47278
|
+
return join5(home(), ".modelstat");
|
|
46941
47279
|
}
|
|
46942
47280
|
function binDir() {
|
|
46943
|
-
return
|
|
47281
|
+
return join5(stateDir(), "bin");
|
|
46944
47282
|
}
|
|
46945
47283
|
function logDir() {
|
|
46946
|
-
return
|
|
47284
|
+
return join5(stateDir(), "logs");
|
|
46947
47285
|
}
|
|
46948
47286
|
function installedCliPath() {
|
|
46949
|
-
return
|
|
47287
|
+
return join5(binDir(), "modelstat.mjs");
|
|
46950
47288
|
}
|
|
46951
47289
|
function runningCliPath() {
|
|
46952
47290
|
return fileURLToPath2(import.meta.url).replace(/service\.(mjs|js|ts)$/, "cli.mjs");
|
|
@@ -46956,7 +47294,7 @@ function installBundle() {
|
|
|
46956
47294
|
mkdirSync2(logDir(), { recursive: true });
|
|
46957
47295
|
const src = runningCliPath();
|
|
46958
47296
|
const dest = installedCliPath();
|
|
46959
|
-
if (!
|
|
47297
|
+
if (!existsSync6(src)) {
|
|
46960
47298
|
throw new Error(
|
|
46961
47299
|
`Can't find the CLI bundle to install from (${src}). Are you running a local dev build?`
|
|
46962
47300
|
);
|
|
@@ -46968,21 +47306,21 @@ function nodeBinary() {
|
|
|
46968
47306
|
return process.execPath;
|
|
46969
47307
|
}
|
|
46970
47308
|
function plistPath() {
|
|
46971
|
-
return
|
|
47309
|
+
return join5(home(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
46972
47310
|
}
|
|
46973
47311
|
function locateTrayExecutable() {
|
|
46974
47312
|
const candidates = [
|
|
46975
|
-
|
|
47313
|
+
join5(home(), "Applications", "ModelstatTray.app", "Contents", "MacOS", "modelstat-tray"),
|
|
46976
47314
|
"/Applications/ModelstatTray.app/Contents/MacOS/modelstat-tray"
|
|
46977
47315
|
];
|
|
46978
47316
|
for (const p of candidates) {
|
|
46979
|
-
if (
|
|
47317
|
+
if (existsSync6(p)) return p;
|
|
46980
47318
|
}
|
|
46981
47319
|
return null;
|
|
46982
47320
|
}
|
|
46983
47321
|
function writePlist(cliPath) {
|
|
46984
47322
|
const p = plistPath();
|
|
46985
|
-
mkdirSync2(
|
|
47323
|
+
mkdirSync2(dirname5(p), { recursive: true });
|
|
46986
47324
|
const tray = locateTrayExecutable();
|
|
46987
47325
|
const programArgs = tray ? ` <string>${tray}</string>` : [
|
|
46988
47326
|
` <string>${nodeBinary()}</string>`,
|
|
@@ -47002,8 +47340,8 @@ ${programArgs}
|
|
|
47002
47340
|
<key>KeepAlive</key>
|
|
47003
47341
|
<dict><key>SuccessfulExit</key><false/></dict>
|
|
47004
47342
|
<key>ThrottleInterval</key><integer>30</integer>
|
|
47005
|
-
<key>StandardOutPath</key><string>${
|
|
47006
|
-
<key>StandardErrorPath</key><string>${
|
|
47343
|
+
<key>StandardOutPath</key><string>${join5(logDir(), "out.log")}</string>
|
|
47344
|
+
<key>StandardErrorPath</key><string>${join5(logDir(), "err.log")}</string>
|
|
47007
47345
|
<key>EnvironmentVariables</key>
|
|
47008
47346
|
<dict>
|
|
47009
47347
|
<key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
@@ -47043,7 +47381,7 @@ function macUninstall() {
|
|
|
47043
47381
|
const target = `gui/${uid}/${SERVICE_LABEL}`;
|
|
47044
47382
|
launchctl(["bootout", target]);
|
|
47045
47383
|
const plist = plistPath();
|
|
47046
|
-
if (
|
|
47384
|
+
if (existsSync6(plist)) {
|
|
47047
47385
|
try {
|
|
47048
47386
|
unlinkSync(plist);
|
|
47049
47387
|
} catch {
|
|
@@ -47056,12 +47394,12 @@ function macStatus() {
|
|
|
47056
47394
|
return { running: r.ok, hint: r.ok ? "launchd managed" : "not installed" };
|
|
47057
47395
|
}
|
|
47058
47396
|
function systemdUnitPath() {
|
|
47059
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
47060
|
-
return
|
|
47397
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? join5(home(), ".config");
|
|
47398
|
+
return join5(xdg, "systemd", "user", `${SYSTEMD_UNIT}.service`);
|
|
47061
47399
|
}
|
|
47062
47400
|
function writeSystemdUnit(cliPath) {
|
|
47063
47401
|
const unitPath = systemdUnitPath();
|
|
47064
|
-
mkdirSync2(
|
|
47402
|
+
mkdirSync2(dirname5(unitPath), { recursive: true });
|
|
47065
47403
|
const unit = `[Unit]
|
|
47066
47404
|
Description=modelstat agent
|
|
47067
47405
|
Documentation=https://modelstat.ai
|
|
@@ -47076,8 +47414,8 @@ RestartSec=10
|
|
|
47076
47414
|
# Don't restart-storm if the service is persistently unreachable.
|
|
47077
47415
|
StartLimitIntervalSec=300
|
|
47078
47416
|
StartLimitBurst=10
|
|
47079
|
-
StandardOutput=append:${
|
|
47080
|
-
StandardError=append:${
|
|
47417
|
+
StandardOutput=append:${join5(logDir(), "out.log")}
|
|
47418
|
+
StandardError=append:${join5(logDir(), "err.log")}
|
|
47081
47419
|
|
|
47082
47420
|
[Install]
|
|
47083
47421
|
WantedBy=default.target
|
|
@@ -47102,7 +47440,7 @@ function linuxInstall() {
|
|
|
47102
47440
|
function linuxUninstall() {
|
|
47103
47441
|
systemctl(["disable", "--now", `${SYSTEMD_UNIT}.service`]);
|
|
47104
47442
|
const unit = systemdUnitPath();
|
|
47105
|
-
if (
|
|
47443
|
+
if (existsSync6(unit)) {
|
|
47106
47444
|
try {
|
|
47107
47445
|
unlinkSync(unit);
|
|
47108
47446
|
} catch {
|
|
@@ -47146,9 +47484,9 @@ function logsDir() {
|
|
|
47146
47484
|
}
|
|
47147
47485
|
function installTrayApp(sourceAppPath) {
|
|
47148
47486
|
if (platform2() !== "darwin") return null;
|
|
47149
|
-
if (!
|
|
47150
|
-
const dest =
|
|
47151
|
-
mkdirSync2(
|
|
47487
|
+
if (!existsSync6(sourceAppPath)) return null;
|
|
47488
|
+
const dest = join5(home(), "Applications", "ModelstatTray.app");
|
|
47489
|
+
mkdirSync2(dirname5(dest), { recursive: true });
|
|
47152
47490
|
spawnSync("rm", ["-rf", dest]);
|
|
47153
47491
|
const r = spawnSync("cp", ["-R", sourceAppPath, dest], { encoding: "utf8" });
|
|
47154
47492
|
if (r.status !== 0) {
|
|
@@ -47158,28 +47496,28 @@ function installTrayApp(sourceAppPath) {
|
|
|
47158
47496
|
}
|
|
47159
47497
|
function bundledTrayAppPath() {
|
|
47160
47498
|
if (platform2() !== "darwin") return null;
|
|
47161
|
-
const here2 =
|
|
47499
|
+
const here2 = dirname5(fileURLToPath2(import.meta.url));
|
|
47162
47500
|
const candidates = [
|
|
47163
47501
|
// Pre-built .app — CI with codesigning drops one here.
|
|
47164
|
-
|
|
47502
|
+
join5(here2, "..", "vendor", "ModelstatTray.app"),
|
|
47165
47503
|
// Local dev layout: apps/agent-dev/src/service.ts → ../../tray-mac/build/ModelstatTray.app
|
|
47166
|
-
|
|
47504
|
+
join5(here2, "..", "..", "tray-mac", "build", "ModelstatTray.app")
|
|
47167
47505
|
];
|
|
47168
47506
|
for (const c of candidates) {
|
|
47169
|
-
if (
|
|
47507
|
+
if (existsSync6(c)) return c;
|
|
47170
47508
|
}
|
|
47171
47509
|
const sourceDirs = [
|
|
47172
|
-
|
|
47173
|
-
|
|
47510
|
+
join5(here2, "..", "vendor", "tray-mac"),
|
|
47511
|
+
join5(here2, "..", "..", "tray-mac")
|
|
47174
47512
|
];
|
|
47175
47513
|
for (const src of sourceDirs) {
|
|
47176
|
-
const build =
|
|
47177
|
-
if (!
|
|
47514
|
+
const build = join5(src, "build-app.sh");
|
|
47515
|
+
if (!existsSync6(build)) continue;
|
|
47178
47516
|
if (!hasSwift()) return null;
|
|
47179
47517
|
const r = spawnSync("bash", [build], { cwd: src, encoding: "utf8" });
|
|
47180
47518
|
if (r.status === 0) {
|
|
47181
|
-
const app =
|
|
47182
|
-
if (
|
|
47519
|
+
const app = join5(src, "build", "ModelstatTray.app");
|
|
47520
|
+
if (existsSync6(app)) return app;
|
|
47183
47521
|
}
|
|
47184
47522
|
}
|
|
47185
47523
|
return null;
|
|
@@ -47411,12 +47749,33 @@ async function cmdConnect(opts) {
|
|
|
47411
47749
|
warn(`tray install skipped: ${e.message}`);
|
|
47412
47750
|
}
|
|
47413
47751
|
}
|
|
47752
|
+
step("Preparing local summariser (downloads on first run)");
|
|
47753
|
+
let modelReady = false;
|
|
47754
|
+
try {
|
|
47755
|
+
const { ensureLlamaModel: ensureLlamaModel2, defaultLlamaConfig: defaultLlamaConfig2 } = await Promise.resolve().then(() => (init_node2(), node_exports));
|
|
47756
|
+
await ensureLlamaModel2(defaultLlamaConfig2());
|
|
47757
|
+
modelReady = true;
|
|
47758
|
+
emitEvent(opts, "summariser_model_ready", {});
|
|
47759
|
+
ok("summariser model on disk");
|
|
47760
|
+
} catch (e) {
|
|
47761
|
+
emitEvent(opts, "summariser_model_failed", {
|
|
47762
|
+
error: e.message
|
|
47763
|
+
});
|
|
47764
|
+
warn(`couldn't prepare summariser model: ${e.message}`);
|
|
47765
|
+
warn(
|
|
47766
|
+
"the background service will retry the download on its first scan"
|
|
47767
|
+
);
|
|
47768
|
+
}
|
|
47414
47769
|
step("Installing background service so the agent survives reboots");
|
|
47415
47770
|
let serviceOk = false;
|
|
47416
47771
|
try {
|
|
47417
47772
|
const svc = installService();
|
|
47418
47773
|
serviceOk = true;
|
|
47419
|
-
emitEvent(opts, "service_installed", {
|
|
47774
|
+
emitEvent(opts, "service_installed", {
|
|
47775
|
+
path: svc.path,
|
|
47776
|
+
logs: svc.logs,
|
|
47777
|
+
summariser_ready: modelReady
|
|
47778
|
+
});
|
|
47420
47779
|
ok(`${platform4() === "darwin" ? "launchd" : "systemd --user"}: ${svc.path}`);
|
|
47421
47780
|
} catch (e) {
|
|
47422
47781
|
emitEvent(opts, "service_install_failed", { error: e.message });
|
|
@@ -47485,6 +47844,9 @@ async function cmdDiscover() {
|
|
|
47485
47844
|
console.log("\u2713 reported to backend");
|
|
47486
47845
|
}
|
|
47487
47846
|
async function cmdScan() {
|
|
47847
|
+
const { preflightSummariser: preflightSummariser2 } = await Promise.resolve().then(() => (init_pipeline2(), pipeline_exports));
|
|
47848
|
+
const sample = await preflightSummariser2();
|
|
47849
|
+
console.log(`[modelstat] summariser preflight ok: "${sample}"`);
|
|
47488
47850
|
const r = await scanAll();
|
|
47489
47851
|
console.log();
|
|
47490
47852
|
console.log(
|