modelstat 0.0.50 → 0.0.52

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.50",
3
+ "version": "0.0.52",
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",
@@ -11,6 +11,7 @@
11
11
  "files": [
12
12
  "dist",
13
13
  "scripts/postinstall.mjs",
14
+ "vendor/ModelstatTray.app",
14
15
  "vendor/tray-mac",
15
16
  "README.md",
16
17
  "LICENSE"
@@ -28,9 +29,9 @@
28
29
  "tsup": "^8.3.5",
29
30
  "tsx": "^4.19.2",
30
31
  "typescript": "^5.7.3",
31
- "@modelstat/companion-core": "0.0.0",
32
+ "@modelstat/parsers": "0.0.0",
32
33
  "@modelstat/core": "0.0.0",
33
- "@modelstat/parsers": "0.0.0"
34
+ "@modelstat/companion-core": "0.0.0"
34
35
  },
35
36
  "engines": {
36
37
  "node": ">=20.18.0"
@@ -0,0 +1,42 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>en</string>
7
+ <key>CFBundleDisplayName</key>
8
+ <string>Modelstat</string>
9
+ <key>CFBundleExecutable</key>
10
+ <string>modelstat-tray</string>
11
+ <key>CFBundleIdentifier</key>
12
+ <string>ai.modelstat.tray</string>
13
+ <key>CFBundleInfoDictionaryVersion</key>
14
+ <string>6.0</string>
15
+ <key>CFBundleName</key>
16
+ <string>Modelstat</string>
17
+ <key>CFBundlePackageType</key>
18
+ <string>APPL</string>
19
+ <key>CFBundleShortVersionString</key>
20
+ <string>0.1.0</string>
21
+ <key>CFBundleVersion</key>
22
+ <string>1</string>
23
+ <key>LSMinimumSystemVersion</key>
24
+ <string>12.0</string>
25
+ <!-- Agent-style app: no Dock icon, no window at launch, just the
26
+ menu-bar item. -->
27
+ <key>LSUIElement</key>
28
+ <true/>
29
+ <!-- Launch at login when the installer drops us into
30
+ ~/Library/LaunchAgents (actually driven by the launchd plist). -->
31
+ <key>LSBackgroundOnly</key>
32
+ <false/>
33
+ <key>NSHighResolutionCapable</key>
34
+ <true/>
35
+ <key>NSSupportsAutomaticTermination</key>
36
+ <false/>
37
+ <key>NSSupportsSuddenTermination</key>
38
+ <true/>
39
+ <key>NSHumanReadableCopyright</key>
40
+ <string>© 2026 modelstat</string>
41
+ </dict>
42
+ </plist>
@@ -0,0 +1 @@
1
+ APPL????
@@ -0,0 +1,115 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>files</key>
6
+ <dict/>
7
+ <key>files2</key>
8
+ <dict/>
9
+ <key>rules</key>
10
+ <dict>
11
+ <key>^Resources/</key>
12
+ <true/>
13
+ <key>^Resources/.*\.lproj/</key>
14
+ <dict>
15
+ <key>optional</key>
16
+ <true/>
17
+ <key>weight</key>
18
+ <real>1000</real>
19
+ </dict>
20
+ <key>^Resources/.*\.lproj/locversion.plist$</key>
21
+ <dict>
22
+ <key>omit</key>
23
+ <true/>
24
+ <key>weight</key>
25
+ <real>1100</real>
26
+ </dict>
27
+ <key>^Resources/Base\.lproj/</key>
28
+ <dict>
29
+ <key>weight</key>
30
+ <real>1010</real>
31
+ </dict>
32
+ <key>^version.plist$</key>
33
+ <true/>
34
+ </dict>
35
+ <key>rules2</key>
36
+ <dict>
37
+ <key>.*\.dSYM($|/)</key>
38
+ <dict>
39
+ <key>weight</key>
40
+ <real>11</real>
41
+ </dict>
42
+ <key>^(.*/)?\.DS_Store$</key>
43
+ <dict>
44
+ <key>omit</key>
45
+ <true/>
46
+ <key>weight</key>
47
+ <real>2000</real>
48
+ </dict>
49
+ <key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
50
+ <dict>
51
+ <key>nested</key>
52
+ <true/>
53
+ <key>weight</key>
54
+ <real>10</real>
55
+ </dict>
56
+ <key>^.*</key>
57
+ <true/>
58
+ <key>^Info\.plist$</key>
59
+ <dict>
60
+ <key>omit</key>
61
+ <true/>
62
+ <key>weight</key>
63
+ <real>20</real>
64
+ </dict>
65
+ <key>^PkgInfo$</key>
66
+ <dict>
67
+ <key>omit</key>
68
+ <true/>
69
+ <key>weight</key>
70
+ <real>20</real>
71
+ </dict>
72
+ <key>^Resources/</key>
73
+ <dict>
74
+ <key>weight</key>
75
+ <real>20</real>
76
+ </dict>
77
+ <key>^Resources/.*\.lproj/</key>
78
+ <dict>
79
+ <key>optional</key>
80
+ <true/>
81
+ <key>weight</key>
82
+ <real>1000</real>
83
+ </dict>
84
+ <key>^Resources/.*\.lproj/locversion.plist$</key>
85
+ <dict>
86
+ <key>omit</key>
87
+ <true/>
88
+ <key>weight</key>
89
+ <real>1100</real>
90
+ </dict>
91
+ <key>^Resources/Base\.lproj/</key>
92
+ <dict>
93
+ <key>weight</key>
94
+ <real>1010</real>
95
+ </dict>
96
+ <key>^[^/]+$</key>
97
+ <dict>
98
+ <key>nested</key>
99
+ <true/>
100
+ <key>weight</key>
101
+ <real>10</real>
102
+ </dict>
103
+ <key>^embedded\.provisionprofile$</key>
104
+ <dict>
105
+ <key>weight</key>
106
+ <real>20</real>
107
+ </dict>
108
+ <key>^version\.plist$</key>
109
+ <dict>
110
+ <key>weight</key>
111
+ <real>20</real>
112
+ </dict>
113
+ </dict>
114
+ </dict>
115
+ </plist>
@@ -275,10 +275,14 @@ final class TrayController: NSObject {
275
275
  }
276
276
  guard !ensureInFlight else { return }
277
277
  ensureInFlight = true
278
- superviseQueue.async { [weak self] in
278
+ superviseQueue.async {
279
279
  // Probe off-main: the health command boots node (~100-300ms).
280
280
  let decision = Self.queryDaemonHealth(cli: cli) ?? "spawn"
281
- DispatchQueue.main.async {
281
+ // Capture self on the closure that actually hops back to @MainActor,
282
+ // not the supervise-queue closure (which only touches statics) — a weak
283
+ // *var* captured across the actor hop is a data race the CI toolchain
284
+ // rejects outright.
285
+ DispatchQueue.main.async { [weak self] in
282
286
  MainActor.assumeIsolated {
283
287
  guard let self else { return }
284
288
  self.ensureInFlight = false
@@ -343,14 +347,17 @@ final class TrayController: NSObject {
343
347
  let err = FileHandle(forWritingAtPath: "\(logsDir)/err.log") ?? FileHandle.standardError
344
348
  p.standardOutput = out
345
349
  p.standardError = err
346
- p.terminationHandler = { [weak self] proc in
350
+ p.terminationHandler = { proc in
347
351
  // Daemon exited — re-converge via the health check, which adopts
348
352
  // a replacement daemon instead of counter-killing it. A clean
349
353
  // sub-5s exit means "another daemon owns the lock" (or an equally
350
354
  // immediate no-op); skip the hot retry and let the 30s watchdog
351
355
  // re-check, so a stale CLI can't put us in a 2s spawn loop.
352
356
  let status = proc.terminationStatus
353
- Task { @MainActor in
357
+ // Capture weak self on the @MainActor Task, not the (non-isolated)
358
+ // terminationHandler — capturing the outer weak var across the actor
359
+ // boundary is a data race the CI toolchain rejects.
360
+ Task { @MainActor [weak self] in
354
361
  guard let self else { return }
355
362
  let uptime = self.daemonSpawnedAt.map { Date().timeIntervalSince($0) } ?? .infinity
356
363
  self.daemon = nil
@@ -442,11 +449,13 @@ final class TrayController: NSObject {
442
449
  return
443
450
  }
444
451
  // Run on a background queue so we don't block the main loop.
445
- DispatchQueue.global(qos: .utility).async { [weak self] in
452
+ DispatchQueue.global(qos: .utility).async {
446
453
  p.waitUntilExit()
447
454
  let data = pipe.fileHandleForReading.readDataToEndOfFile()
448
455
  let stats = try? JSONDecoder().decode(AgentStats.self, from: data)
449
- DispatchQueue.main.async {
456
+ // Hand self to the main-queue closure (which mutates @MainActor state),
457
+ // not the background closure that only does blocking IO.
458
+ DispatchQueue.main.async { [weak self] in
450
459
  self?.latest = stats
451
460
  self?.renderStats()
452
461
  }
@@ -2,16 +2,24 @@
2
2
  # Build ModelstatTray.app from the Swift package.
3
3
  #
4
4
  # Output:
5
- # .build/release/modelstat-tray (raw executable)
6
- # build/ModelstatTray.app (bundle ready to drop into /Applications)
5
+ # build/ModelstatTray.app (ad-hoc-signed bundle, ready to run/ship)
7
6
  #
8
- # Usage:
9
- # ./build-app.sh (release build, universal when running on arm64)
10
- # SWIFT_ARCH=x86_64 ./build-app.sh (cross-build for Intel)
7
+ # Modes (env):
8
+ # TRAY_BUILD_CONFIG=release|debug build config (default: release)
9
+ # TRAY_UNIVERSAL=1 arm64 + x86_64 fat (default: host arch)
10
+ # — universal needs FULL Xcode (xcbuild);
11
+ # host-arch builds on Command Line Tools
12
+ # alone, which is the on-device fallback.
11
13
  #
12
- # We deliberately DO NOT codesign here the installer pipeline takes
13
- # care of that with the team's Developer ID. This script just has to
14
- # produce a runnable bundle; `codesign` + `create-dmg` happen in CI.
14
+ # Signing: we ad-hoc sign (`codesign -s -`). That is enough to RUN — arm64
15
+ # requires at least an ad-hoc signature, and an app delivered inside the npm
16
+ # tarball is not quarantined, so Gatekeeper's notarization gate never fires.
17
+ # To distribute a DOWNLOADED build (a DMG off the website) without a Gatekeeper
18
+ # prompt, re-sign with a Developer ID cert + Hardened Runtime and notarize:
19
+ # codesign --force --options runtime --timestamp \
20
+ # --sign "Developer ID Application: …" build/ModelstatTray.app
21
+ # xcrun notarytool submit … && xcrun stapler staple build/ModelstatTray.app
22
+ # That is purely additive and slots into CI once the Apple account exists.
15
23
 
16
24
  set -euo pipefail
17
25
 
@@ -20,24 +28,52 @@ cd "$HERE"
20
28
 
21
29
  APP_NAME="ModelstatTray"
22
30
  BUNDLE="build/${APP_NAME}.app"
31
+ CONFIG="${TRAY_BUILD_CONFIG:-release}"
23
32
 
24
- # Clean prior bundle; leave the .build/ cache so incremental swift
25
- # compiles stay fast.
26
- rm -rf "$BUNDLE"
27
- mkdir -p "$BUNDLE/Contents/MacOS" "$BUNDLE/Contents/Resources"
33
+ BUILD_ARGS=(-c "$CONFIG")
34
+ if [[ "${TRAY_UNIVERSAL:-}" == "1" ]]; then
35
+ BUILD_ARGS+=(--arch arm64 --arch x86_64)
36
+ fi
37
+
38
+ echo "▶ swift build ${BUILD_ARGS[*]}"
39
+ swift build "${BUILD_ARGS[@]}"
28
40
 
29
- echo "▶ swift build -c release"
30
- swift build -c release
41
+ # Locate the produced binary. Universal builds land under
42
+ # .build/apple/Products/<Config>/; single-arch under .build/<config>/ (a
43
+ # triple-prefixed dir that SwiftPM also symlinks as .build/<config>).
44
+ BIN=""
45
+ for cand in \
46
+ ".build/apple/Products/Release/modelstat-tray" \
47
+ ".build/apple/Products/Debug/modelstat-tray" \
48
+ ".build/${CONFIG}/modelstat-tray" \
49
+ ".build/release/modelstat-tray" \
50
+ ".build/debug/modelstat-tray"; do
51
+ if [[ -f "$cand" ]]; then BIN="$cand"; break; fi
52
+ done
53
+ if [[ -z "$BIN" ]]; then
54
+ echo "✗ could not find a built modelstat-tray binary under .build/" >&2
55
+ exit 1
56
+ fi
57
+ echo "▶ binary: $BIN"
58
+ lipo -archs "$BIN" 2>/dev/null | sed 's/^/ arches: /' || true
31
59
 
32
- cp ".build/release/modelstat-tray" "$BUNDLE/Contents/MacOS/modelstat-tray"
60
+ # Assemble the bundle fresh (leave .build/ so incremental compiles stay fast).
61
+ rm -rf "$BUNDLE"
62
+ mkdir -p "$BUNDLE/Contents/MacOS" "$BUNDLE/Contents/Resources"
63
+ cp "$BIN" "$BUNDLE/Contents/MacOS/modelstat-tray"
33
64
  chmod +x "$BUNDLE/Contents/MacOS/modelstat-tray"
34
65
  cp "Resources/Info.plist" "$BUNDLE/Contents/Info.plist"
35
66
 
36
- # Embedded PkgInfo file — AppKit used to be picky about this. Costs
37
- # four bytes to include and silences a startup warning on older macOS.
67
+ # Embedded PkgInfo file — AppKit used to be picky about this. Costs four
68
+ # bytes to include and silences a startup warning on older macOS.
38
69
  printf "APPL????" > "$BUNDLE/Contents/PkgInfo"
39
70
 
71
+ # Ad-hoc sign the assembled bundle (seals it; required to launch on arm64).
72
+ echo "▶ codesign --force --sign - (ad-hoc)"
73
+ codesign --force --sign - "$BUNDLE"
74
+
40
75
  echo
41
76
  echo "✓ Built $BUNDLE"
77
+ codesign -dv "$BUNDLE" 2>&1 | grep -iE "signature|identifier" | sed 's/^/ /' || true
42
78
  echo " run: open '$BUNDLE'"
43
79
  echo " install: cp -R '$BUNDLE' /Applications/"