modelstat 0.0.25 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +474 -121
- package/dist/cli.mjs.map +1 -1
- package/package.json +4 -1
- package/scripts/postinstall.mjs +120 -0
- package/vendor/tray-mac/Sources/ModelstatTray/main.swift +24 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelstat",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"description": "modelstat companion — reads local AI-tool usage and ships tokenised events to modelstat.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"dist",
|
|
13
|
+
"scripts/postinstall.mjs",
|
|
13
14
|
"vendor/tray-mac",
|
|
14
15
|
"README.md",
|
|
15
16
|
"LICENSE"
|
|
@@ -24,12 +25,14 @@
|
|
|
24
25
|
"prepack": "pnpm run build && pnpm run build:tray",
|
|
25
26
|
"pack:tarball": "pnpm run build && npm pack --pack-destination ../..",
|
|
26
27
|
"install:local": "bash ./scripts/install-local.sh",
|
|
28
|
+
"postinstall": "node ./scripts/postinstall.mjs",
|
|
27
29
|
"typecheck": "tsc --noEmit"
|
|
28
30
|
},
|
|
29
31
|
"dependencies": {
|
|
30
32
|
"chokidar": "^4.0.3",
|
|
31
33
|
"conf": "^13.1.0",
|
|
32
34
|
"dotenv": "^16.4.7",
|
|
35
|
+
"node-llama-cpp": "^3.18.0",
|
|
33
36
|
"ulid": "^2.3.0",
|
|
34
37
|
"undici": "^7.1.0"
|
|
35
38
|
},
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Post-install hook for the @modelstat/agent CLI.
|
|
4
|
+
*
|
|
5
|
+
* Runs after `npm install -g modelstat` (or `npx modelstat` cache
|
|
6
|
+
* population) and downloads the bundled summariser GGUF (~2.7 GB)
|
|
7
|
+
* with visible progress, so the user sees the heavy download
|
|
8
|
+
* attached to the install they just kicked off — not as a surprise
|
|
9
|
+
* 7-minute hang the first time they run `modelstat scan`.
|
|
10
|
+
*
|
|
11
|
+
* Skipped when:
|
|
12
|
+
* - Running inside this repo's pnpm workspace (we don't want every
|
|
13
|
+
* dev `pnpm install` to pull 2.7 GB; the developer can opt in by
|
|
14
|
+
* running `modelstat connect` once or pre-pulling the model).
|
|
15
|
+
* - `MODELSTAT_SKIP_POSTINSTALL=1` is set (CI escape hatch, or for
|
|
16
|
+
* packagers who handle the model out-of-band).
|
|
17
|
+
* - `CI=true` is set (most CI providers; avoids blocking automated
|
|
18
|
+
* installs on a multi-GB pull).
|
|
19
|
+
* - stdout isn't a TTY AND CI isn't set (likely a scripted
|
|
20
|
+
* install that doesn't want long-running side-effects).
|
|
21
|
+
*
|
|
22
|
+
* In all skip cases the model still downloads lazily on first
|
|
23
|
+
* `modelstat scan` — the agent always preflights the summariser
|
|
24
|
+
* before producing any segments (see src/pipeline.ts), so users in
|
|
25
|
+
* skip-mode just see the download then, instead of now.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { existsSync } from "node:fs";
|
|
29
|
+
import { dirname, join, resolve } from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
|
|
32
|
+
function inThisMonorepo() {
|
|
33
|
+
// Walk up from this script looking for the repo's pnpm-workspace.yaml.
|
|
34
|
+
// When npm installs us into a global node_modules, this file isn't an
|
|
35
|
+
// ancestor — so we know we're in a real install and should download.
|
|
36
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
for (let i = 0; i < 10; i++) {
|
|
38
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml"))) return true;
|
|
39
|
+
const up = resolve(dir, "..");
|
|
40
|
+
if (up === dir) return false;
|
|
41
|
+
dir = up;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function main() {
|
|
47
|
+
if (process.env.MODELSTAT_SKIP_POSTINSTALL === "1") {
|
|
48
|
+
console.log(
|
|
49
|
+
"[modelstat] postinstall skipped (MODELSTAT_SKIP_POSTINSTALL=1) — model downloads lazily on first scan",
|
|
50
|
+
);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (process.env.CI === "true" || process.env.CI === "1") {
|
|
54
|
+
console.log(
|
|
55
|
+
"[modelstat] postinstall skipped (CI=1) — model downloads lazily on first scan",
|
|
56
|
+
);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (inThisMonorepo()) {
|
|
60
|
+
// Dev environment — don't pull 2.7 GB on every workspace install.
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!process.stdout.isTTY) {
|
|
64
|
+
console.log(
|
|
65
|
+
"[modelstat] postinstall: non-TTY — model will download lazily on first `modelstat scan`",
|
|
66
|
+
);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Resolve the helper from the workspace package the bundle uses.
|
|
71
|
+
// companion-core is bundled into dist/cli.mjs by tsup, but we need
|
|
72
|
+
// the helper as a standalone import here (postinstall runs against
|
|
73
|
+
// the unbundled source layout).
|
|
74
|
+
let ensureLlamaModel;
|
|
75
|
+
let defaultLlamaConfig;
|
|
76
|
+
try {
|
|
77
|
+
({ ensureLlamaModel, defaultLlamaConfig } = await import(
|
|
78
|
+
"@modelstat/companion-core/node"
|
|
79
|
+
));
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.warn(
|
|
82
|
+
`[modelstat] postinstall: couldn't import summariser helper (${err && err.message ? err.message : err}); model will download lazily on first scan`,
|
|
83
|
+
);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log("");
|
|
88
|
+
console.log("━".repeat(60));
|
|
89
|
+
console.log(" Pre-downloading the local summariser model");
|
|
90
|
+
console.log("");
|
|
91
|
+
console.log(" modelstat ships with a small LLM that runs ON YOUR MACHINE");
|
|
92
|
+
console.log(" to summarise every coding session. The model is ~2.7 GB —");
|
|
93
|
+
console.log(" pulling it now so the first `modelstat scan` is instant.");
|
|
94
|
+
console.log("━".repeat(60));
|
|
95
|
+
console.log("");
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await ensureLlamaModel(defaultLlamaConfig());
|
|
99
|
+
console.log("");
|
|
100
|
+
console.log("[modelstat] ✓ summariser ready — run `modelstat connect` next");
|
|
101
|
+
} catch (err) {
|
|
102
|
+
// Don't fail the npm install. The model will retry-download on
|
|
103
|
+
// first scan; better to leave the user with a working binary
|
|
104
|
+
// than to abort because their network blipped.
|
|
105
|
+
console.warn(
|
|
106
|
+
`[modelstat] ⚠ couldn't pre-download summariser (${err && err.message ? err.message : err})`,
|
|
107
|
+
);
|
|
108
|
+
console.warn(
|
|
109
|
+
"[modelstat] the model will download lazily on first `modelstat scan` instead",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
main().catch((err) => {
|
|
115
|
+
console.warn(
|
|
116
|
+
`[modelstat] postinstall failed: ${err && err.message ? err.message : err}`,
|
|
117
|
+
);
|
|
118
|
+
// Never fail the install for a postinstall problem.
|
|
119
|
+
process.exit(0);
|
|
120
|
+
});
|
|
@@ -149,12 +149,17 @@ final class TrayController: NSObject {
|
|
|
149
149
|
return
|
|
150
150
|
}
|
|
151
151
|
let p = Process()
|
|
152
|
+
// --force: the tray owns the daemon. If a stale lock from a prior
|
|
153
|
+
// run (crash, kill -9, OS reboot mid-write) is left behind, the
|
|
154
|
+
// unforced `start` exits in <1s with "already running" and the
|
|
155
|
+
// tray's terminationHandler retries it forever. With --force we
|
|
156
|
+
// claim the lock unconditionally and become the live daemon.
|
|
152
157
|
if cli.pathExtension == "mjs" {
|
|
153
158
|
p.launchPath = "/usr/bin/env"
|
|
154
|
-
p.arguments = ["node", cli.path, "start"]
|
|
159
|
+
p.arguments = ["node", cli.path, "start", "--force"]
|
|
155
160
|
} else {
|
|
156
161
|
p.launchPath = cli.path
|
|
157
|
-
p.arguments = ["start"]
|
|
162
|
+
p.arguments = ["start", "--force"]
|
|
158
163
|
}
|
|
159
164
|
// Bolt stdout/stderr onto the same log the launchd plist uses so
|
|
160
165
|
// `modelstat status` still sees the same tail.
|
|
@@ -208,6 +213,10 @@ final class TrayController: NSObject {
|
|
|
208
213
|
do {
|
|
209
214
|
try p.run()
|
|
210
215
|
} catch {
|
|
216
|
+
// Surface the failure in the menu instead of leaving the title
|
|
217
|
+
// stuck on whatever it was last (e.g. "Starting…" forever). Most
|
|
218
|
+
// likely cause is `node` not being on the launchd-inherited PATH.
|
|
219
|
+
statusMI.title = "stats failed: \(error.localizedDescription)"
|
|
211
220
|
return
|
|
212
221
|
}
|
|
213
222
|
// Run on a background queue so we don't block the main loop.
|
|
@@ -336,11 +345,23 @@ final class TrayController: NSObject {
|
|
|
336
345
|
// TrayController is @MainActor, so its init must run on the main
|
|
337
346
|
// actor. We wrap the bootstrap in a main-actor function to satisfy
|
|
338
347
|
// Swift 6's strict concurrency without bloating the controller.
|
|
348
|
+
//
|
|
349
|
+
// IMPORTANT: nothing in AppKit retains TrayController for us. The
|
|
350
|
+
// NSStatusItem holds the menu, and NSMenuItem.target is a weak
|
|
351
|
+
// reference, so the controller has no strong owners. Without a
|
|
352
|
+
// global anchor, ARC deallocates the controller as soon as init
|
|
353
|
+
// returns — which leaves the timer's `[weak self]` callback firing
|
|
354
|
+
// against nil and the menu title frozen on "Starting…" forever.
|
|
355
|
+
// The `controller` global below is the strong reference that keeps
|
|
356
|
+
// the controller alive for the entire app lifetime.
|
|
357
|
+
@MainActor
|
|
358
|
+
private var controller: TrayController?
|
|
359
|
+
|
|
339
360
|
@MainActor
|
|
340
361
|
func bootstrap() {
|
|
341
362
|
let app = NSApplication.shared
|
|
342
363
|
app.setActivationPolicy(.accessory)
|
|
343
|
-
|
|
364
|
+
controller = TrayController()
|
|
344
365
|
app.run()
|
|
345
366
|
}
|
|
346
367
|
|