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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelstat",
3
- "version": "0.0.24",
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,28 +10,43 @@
10
10
  },
11
11
  "files": [
12
12
  "dist",
13
+ "scripts/postinstall.mjs",
13
14
  "vendor/tray-mac",
14
15
  "README.md",
15
16
  "LICENSE"
16
17
  ],
18
+ "scripts": {
19
+ "dev:connect": "tsx ./src/cli.ts connect",
20
+ "dev:scan": "tsx ./src/cli.ts scan",
21
+ "dev:watch": "tsx ./src/cli.ts watch",
22
+ "dev:discover": "tsx ./src/cli.ts discover",
23
+ "build": "tsup",
24
+ "build:tray": "bash ./scripts/build-tray.sh",
25
+ "prepack": "pnpm run build && pnpm run build:tray",
26
+ "pack:tarball": "pnpm run build && npm pack --pack-destination ../..",
27
+ "install:local": "bash ./scripts/install-local.sh",
28
+ "postinstall": "node ./scripts/postinstall.mjs",
29
+ "typecheck": "tsc --noEmit"
30
+ },
17
31
  "dependencies": {
18
32
  "chokidar": "^4.0.3",
19
33
  "conf": "^13.1.0",
20
34
  "dotenv": "^16.4.7",
35
+ "node-llama-cpp": "^3.18.0",
21
36
  "ulid": "^2.3.0",
22
37
  "undici": "^7.1.0"
23
38
  },
24
39
  "devDependencies": {
40
+ "@modelstat/companion-core": "workspace:*",
41
+ "@modelstat/core": "workspace:*",
42
+ "@modelstat/parsers": "workspace:*",
25
43
  "@types/node": "^22.10.5",
26
44
  "tsup": "^8.3.5",
27
45
  "tsx": "^4.19.2",
28
- "typescript": "^5.7.3",
29
- "@modelstat/companion-core": "0.0.0",
30
- "@modelstat/core": "0.0.0",
31
- "@modelstat/parsers": "0.0.0"
46
+ "typescript": "^5.7.3"
32
47
  },
33
48
  "engines": {
34
- "node": ">=20.0.0"
49
+ "node": ">=20.18.0"
35
50
  },
36
51
  "os": [
37
52
  "darwin",
@@ -56,16 +71,5 @@
56
71
  "type": "git",
57
72
  "url": "git+https://github.com/modelstat/modelstat.git",
58
73
  "directory": "apps/agent-dev"
59
- },
60
- "scripts": {
61
- "dev:connect": "tsx ./src/cli.ts connect",
62
- "dev:scan": "tsx ./src/cli.ts scan",
63
- "dev:watch": "tsx ./src/cli.ts watch",
64
- "dev:discover": "tsx ./src/cli.ts discover",
65
- "build": "tsup",
66
- "build:tray": "bash ./scripts/build-tray.sh",
67
- "pack:tarball": "pnpm run build && npm pack --pack-destination ../..",
68
- "install:local": "bash ./scripts/install-local.sh",
69
- "typecheck": "tsc --noEmit"
70
74
  }
71
- }
75
+ }
@@ -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
- _ = TrayController()
364
+ controller = TrayController()
344
365
  app.run()
345
366
  }
346
367
 
File without changes
package/LICENSE DELETED
@@ -1,87 +0,0 @@
1
- Copyright (c) 2026 ModelState Inc
2
-
3
- Source-Available License
4
-
5
- 1. Grant of Rights
6
-
7
- Subject to the terms of this License, you are granted a non-exclusive, worldwide,
8
- non-transferable, non-sublicensable license to:
9
-
10
- - View, read, and inspect the source code
11
- - Build, modify, and run the software
12
- - Use the software internally, including in production, solely to access or interact
13
- with ModelState Inc’s hosted services
14
-
15
- 2. Permitted Use
16
-
17
- You may use the software as a client, agent, or self-hosted component that connects
18
- to and depends on ModelState Inc’s cloud or hosted services.
19
-
20
- 3. Restrictions
21
-
22
- You may NOT, without explicit prior written permission from ModelState Inc:
23
-
24
- - Use the software to provide a hosted, managed, or SaaS service to third parties
25
- - Use the software in any product or service that competes with ModelState Inc
26
- - Use the software to build or operate an alternative to ModelState Inc’s services
27
- - Redistribute, sublicense, sell, license, or commercially exploit the software
28
- - Offer the software (modified or unmodified) as part of a commercial offering
29
- - Make the software available to third parties as a service
30
- - Use the software for the benefit of third parties (including multi-tenant or shared environments)
31
- - Circumvent or attempt to circumvent the limitations of this License
32
- - Remove or alter any licensing, copyright, or attribution notices
33
-
34
- 4. Definition of Competing Service
35
-
36
- “Competing Service” means any product or service that provides substantially similar
37
- functionality to ModelState Inc’s offerings, including but not limited to:
38
-
39
- - AI or LLM usage tracking, monitoring, or observability systems
40
- - Model analytics platforms or dashboards
41
- - Inference tracking, logging, or telemetry pipelines
42
- - Evaluation, benchmarking, or quality analysis systems for AI/ML models
43
- - Cost tracking, performance tracking, or optimization systems for model inference
44
- - Any system that collects, processes, analyzes, or visualizes usage or behavior of AI or machine learning models, including large language models (LLMs), when offered as a product or service
45
-
46
- 5. Internal Use
47
-
48
- Use of the software is permitted only for your internal business or personal use,
49
- and not for the benefit of third parties.
50
-
51
- 6. Network Use Restriction
52
-
53
- You may not use the software to expose APIs, endpoints, dashboards, or services to external
54
- users except as part of accessing ModelState Inc’s services.
55
-
56
- 7. Data Extraction Restriction
57
-
58
- You may not use the software to replicate, extract, reverse engineer, or reconstruct
59
- ModelState Inc’s service behavior, APIs, data models, or system design for the purpose
60
- of building, improving, or operating a competing system or service.
61
-
62
- 8. Ownership
63
-
64
- All rights, title, and interest in the software remain exclusively with ModelState Inc.
65
- The software is licensed, not sold.
66
-
67
- 9. Contributions
68
-
69
- Unless explicitly agreed otherwise in writing, any contributions submitted to the software
70
- grant ModelState Inc a perpetual, irrevocable, worldwide, royalty-free license to use,
71
- modify, and distribute those contributions.
72
-
73
- 10. Termination
74
-
75
- This License terminates automatically if you violate any of its terms.
76
- Upon termination, you must immediately cease all use and delete all copies of the software.
77
-
78
- 11. Disclaimer of Warranty
79
-
80
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
81
- EXPRESS OR IMPLIED.
82
-
83
- 12. Limitation of Liability
84
-
85
- IN NO EVENT SHALL MODELSTATE INC BE LIABLE FOR ANY CLAIM, DAMAGES,
86
- OR OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE
87
- OR ITS USE OR OTHER DEALINGS IN THE SOFTWARE.