@meridiona/meridian-darwin-arm64 1.63.0 → 1.64.0

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/VERSION CHANGED
@@ -1 +1 @@
1
- 1.63.0
1
+ 1.64.0
package/bin/meridian CHANGED
Binary file
package/bin/meridian-tray CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meridiona/meridian-darwin-arm64",
3
- "version": "1.63.0",
3
+ "version": "1.64.0",
4
4
  "description": "Prebuilt Meridian app for macOS arm64 (daemon binary + dashboard + Python services). Installed via @meridiona/meridian.",
5
5
  "homepage": "https://github.com/Meridiona/meridian",
6
6
  "repository": {
@@ -10,9 +10,14 @@
10
10
  set -euo pipefail
11
11
 
12
12
  APP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
13
+ # ⚠️ LICENSE PIN — DO NOT BUMP past 0.4.6 without legal review.
14
+ # screenpipe relicensed MIT → Commercial on 2026-06-10. 0.4.6 (published
15
+ # 2026-06-05) is the LAST MIT npm release; >= 0.4.17 (2026-06-11+) is Commercial,
16
+ # whose "competing product" clause would then bind our users. Meridian ships zero
17
+ # screenpipe code, so MIT 0.4.6 keeps the install license-clean. Bumping this is a
18
+ # deliberate legal decision. CI enforces it — .github/workflows/ci.yml `screenpipe-license-pin`.
13
19
  SCREENPIPE_VERSION="0.4.6"
14
20
  MLX_PORT="${MLX_PORT:-7823}"
15
- UI_PORT="${MERIDIAN_UI_PORT:-3939}" # dashboard port (override via MERIDIAN_UI_PORT)
16
21
  SKIP_PERMISSIONS=0
17
22
  [[ "${1:-}" == "--skip-permissions" ]] && SKIP_PERMISSIONS=1
18
23
 
@@ -22,7 +27,6 @@ SKIP_PERMISSIONS=0
22
27
  _HASH_FILE="${HOME}/.meridian/.component-hashes"
23
28
  _load_old_hash() { grep "^$1=" "${_HASH_FILE}" 2>/dev/null | cut -d= -f2 || true; }
24
29
  _OLD_DAEMON_HASH="$(_load_old_hash daemon_bin)"
25
- _OLD_UI_HASH="$(_load_old_hash ui_tarball)"
26
30
  _OLD_TRAY_HASH="$(_load_old_hash tray_bin)"
27
31
 
28
32
  info() { echo "→ $*" >&2; }
@@ -125,111 +129,28 @@ source "${APP_ROOT}/scripts/lib-trello-setup.sh"
125
129
  GUI_TARGET="gui/$(id -u)"
126
130
  LAUNCH_AGENTS="${HOME}/Library/LaunchAgents"
127
131
 
128
- # Resolve the Node runtime the dashboard must run on. The better-sqlite3 addon in
129
- # ui.tar.gz is built against one exact Node version (recorded in
130
- # bin/node-runtime.meta by package-release.sh); running any other Node major
131
- # triggers a NODE_MODULE_VERSION (ABI) mismatch and the dashboard crash-loops.
132
- # The 113 MB Node binary is NOT shipped through npm (it would blow the registry
133
- # payload limit), so we download that exact official build from nodejs.org once,
134
- # verify the pinned SHA-256, and cache it under ~/.meridian (survives the APP_ROOT
135
- # rm-rf on `meridian update`). Echoes the node path on stdout. Failure fallbacks
136
- # to system node are LOUD because they may not match the addon's ABI.
137
- resolve_node_runtime() {
138
- local meta="${APP_ROOT}/bin/node-runtime.meta"
139
- # Dev/source install (no meta file): use system/Homebrew node as-is. Such a
140
- # build compiles its own better-sqlite3 against that node, so ABI matches.
141
- if [[ ! -f "${meta}" ]]; then
142
- local _n
143
- for _n in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
144
- [[ -x "${_n}" ]] && { echo "${_n}"; return 0; }
145
- done
146
- return 1
147
- fi
148
- local ver sha
149
- ver="$(grep '^NODE_RUNTIME_VERSION=' "${meta}" | cut -d= -f2 | tr -d '[:space:]')"
150
- sha="$(grep '^NODE_RUNTIME_SHA=' "${meta}" | cut -d= -f2 | tr -d '[:space:]')"
151
- if [[ -z "${ver}" || -z "${sha}" ]]; then
152
- warn "node-runtime.meta is malformed (missing VERSION or SHA) — falling back to system node"
153
- for _n in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
154
- [[ -x "${_n}" ]] && { echo "${_n}"; return 0; }
155
- done
156
- return 1
157
- fi
158
- local cache_dir="${HOME}/.meridian/node-runtime/v${ver}"
159
- local cache_bin="${cache_dir}/bin/node"
160
- if [[ -x "${cache_bin}" ]]; then echo "${cache_bin}"; return 0; fi
161
- local tmp tgz url got
162
- tmp="$(mktemp -d)"; tgz="${tmp}/node.tar.gz"
163
- url="https://nodejs.org/dist/v${ver}/node-v${ver}-darwin-arm64.tar.gz"
164
- info "Downloading Node ${ver} runtime for the dashboard (one-time, ~40 MB)…"
165
- if curl -fsSL --retry 3 "${url}" -o "${tgz}"; then
166
- got="$(shasum -a 256 "${tgz}" | cut -d' ' -f1)"
167
- if [[ "${got}" == "${sha}" ]]; then
168
- tar -xzf "${tgz}" -C "${tmp}"
169
- rm -rf "${cache_dir}"; mkdir -p "$(dirname "${cache_dir}")"
170
- mv "${tmp}/node-v${ver}-darwin-arm64" "${cache_dir}"
171
- rm -rf "${tmp}"
172
- ok "Node ${ver} runtime cached (ABI-matched to the dashboard)"
173
- echo "${cache_bin}"; return 0
174
- fi
175
- warn "Node ${ver} SHA-256 mismatch (expected ${sha}, got ${got}) — not using it"
176
- else
177
- warn "Node ${ver} download failed (offline?) — the dashboard needs it to match better-sqlite3's ABI"
178
- fi
179
- rm -rf "${tmp}"
180
- local _n
181
- for _n in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
182
- if [[ -x "${_n}" ]]; then
183
- warn "Falling back to ${_n} — if the dashboard fails to load, re-run 'meridian update' with a connection"
184
- echo "${_n}"; return 0
185
- fi
186
- done
187
- return 1
188
- }
189
-
190
- # Register the dashboard as a launchd agent that runs the prebuilt Next.js
191
- # standalone server (`node ui/server.js`) — no `npm start`, no node_modules
192
- # install. Mirrors the EIO-safe bootout/bootstrap pattern of the other agents.
193
- install_ui_standalone() {
132
+ # Retire the legacy standalone dashboard Node server. The dashboard now ships
133
+ # embedded INSIDE the tray binary (static export, no Node server), so any
134
+ # `com.meridiona.ui` launchd agent from a pre-fold install is a zombie — a
135
+ # KeepAlive=true Node server holding the old dashboard port. Boot it out + remove
136
+ # its plist, and drop the now-orphaned ABI-matched Node runtime cache. Idempotent
137
+ # (no-op when absent) and non-fatal (a launchctl hiccup must not abort the
138
+ # update) but logged, never silent.
139
+ retire_legacy_ui_server() {
194
140
  local label="com.meridiona.ui"
195
141
  local plist="${LAUNCH_AGENTS}/${label}.plist"
196
- # Resolve the ABI-matched Node runtime (downloads + caches it on first use).
197
- local node_bin
198
- node_bin="$(resolve_node_runtime)" || { err "node not found — install Node.js: brew install node"; return 1; }
199
- local start_script="${APP_ROOT}/scripts/ui-start.sh"
200
- chmod +x "${start_script}" 2>/dev/null || true
201
- mkdir -p "${HOME}/.meridian/logs" "${LAUNCH_AGENTS}"
202
- cat > "${plist}" <<PLIST
203
- <?xml version="1.0" encoding="UTF-8"?>
204
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
205
- <plist version="1.0"><dict>
206
- <key>Label</key><string>${label}</string>
207
- <key>ProgramArguments</key>
208
- <array><string>${start_script}</string></array>
209
- <key>WorkingDirectory</key><string>${APP_ROOT}/ui</string>
210
- <key>EnvironmentVariables</key>
211
- <dict>
212
- <key>PORT</key><string>${UI_PORT}</string>
213
- <key>HOSTNAME</key><string>127.0.0.1</string>
214
- <key>MERIDIAN_DB_PATH</key><string>${HOME}/.meridian/meridian.db</string>
215
- <key>MERIDIAN_NODE_BIN</key><string>${node_bin}</string>
216
- </dict>
217
- <key>RunAtLoad</key><true/>
218
- <key>KeepAlive</key><true/>
219
- <key>StandardOutPath</key><string>${HOME}/.meridian/logs/ui.log</string>
220
- <key>StandardErrorPath</key><string>${HOME}/.meridian/logs/ui-error.log</string>
221
- <key>ProcessType</key><string>Background</string>
222
- </dict></plist>
223
- PLIST
224
- plutil -lint "${plist}" >/dev/null 2>&1 || { warn "ui plist failed lint"; return 1; }
225
- launchctl bootout "${GUI_TARGET}/${label}" 2>/dev/null || true
226
- local w=0
227
- while launchctl print "${GUI_TARGET}/${label}" >/dev/null 2>&1; do
228
- sleep 1; w=$((w+1)); [[ $w -ge 15 ]] && break
229
- done
230
- launchctl enable "${GUI_TARGET}/${label}" 2>/dev/null || true
231
- launchctl bootstrap "${GUI_TARGET}" "${plist}"
232
- launchctl kickstart -k "${GUI_TARGET}/${label}" 2>/dev/null || true
142
+ if launchctl print "${GUI_TARGET}/${label}" >/dev/null 2>&1; then
143
+ info "Retiring the legacy dashboard server (now bundled in the tray app)…"
144
+ launchctl bootout "${GUI_TARGET}/${label}" 2>/dev/null \
145
+ || warn "could not bootout ${label} (continuing)"
146
+ fi
147
+ if [[ -f "${plist}" ]]; then
148
+ rm -f "${plist}" && ok "removed legacy ${label} launchd agent" \
149
+ || warn "could not remove ${plist} (continuing)"
150
+ fi
151
+ # The pinned Node runtime existed only to ABI-match better-sqlite3 in the old
152
+ # server; nothing uses it now.
153
+ rm -rf "${HOME}/.meridian/node-runtime" 2>/dev/null || true
233
154
  }
234
155
 
235
156
  # Enable accessibility mode in VS Code / Cursor / Antigravity so screenpipe
@@ -339,10 +260,10 @@ ok "node + python ($(${PYTHON_BIN} --version 2>&1)) + uv ($(${UV_BIN} --version
339
260
  if ! command -v screenpipe >/dev/null 2>&1; then
340
261
  info "Installing screenpipe ${SCREENPIPE_VERSION} via npm…"
341
262
  if npm_global_writable; then
342
- npm install -g "screenpipe@${SCREENPIPE_VERSION}"
263
+ npm install -g --ignore-scripts "screenpipe@${SCREENPIPE_VERSION}"
343
264
  else
344
265
  warn "global npm prefix needs root — elevating just this install (you may be prompted)…"
345
- sudo npm install -g "screenpipe@${SCREENPIPE_VERSION}"
266
+ sudo npm install -g --ignore-scripts "screenpipe@${SCREENPIPE_VERSION}"
346
267
  fi
347
268
  fi
348
269
  ok "screenpipe"
@@ -367,22 +288,18 @@ fi
367
288
  if ! command -v ffmpeg >/dev/null 2>&1; then info "Installing ffmpeg…"; brew install ffmpeg; fi
368
289
  ok "ffmpeg"
369
290
 
370
- # ── 2. Config: single repo-local .env ────────────────────────────────────────
371
- ENV_FILE="${APP_ROOT}/.env"
291
+ # ── 2. Config: user credential file ──────────────────────────────────────────
292
+ # Canonical location is ~/.meridian/.env — install-independent, next to
293
+ # meridian.db and settings.json, never inside app/ (the binary tree).
294
+ ENV_FILE="${HOME}/.meridian/.env"
372
295
  if [[ ! -f "${ENV_FILE}" ]]; then
373
296
  cp "${APP_ROOT}/.env.example" "${ENV_FILE}"
374
- info "created ${ENV_FILE} from template — add your Jira creds later: meridian config edit"
297
+ info "created ${ENV_FILE} from template — add your credentials: meridian config edit"
375
298
  fi
376
299
  # MLX is the default backend.
377
300
  grep -q '^CLASSIFIER_BACKEND=' "${ENV_FILE}" || echo "CLASSIFIER_BACKEND=mlx" >> "${ENV_FILE}"
378
301
  grep -q '^MLX_SERVER_PORT=' "${ENV_FILE}" || echo "MLX_SERVER_PORT=${MLX_PORT}" >> "${ENV_FILE}"
379
- # Dashboard port — honour an existing .env value, otherwise record the default.
380
- if grep -q '^MERIDIAN_UI_PORT=' "${ENV_FILE}"; then
381
- UI_PORT="$(grep '^MERIDIAN_UI_PORT=' "${ENV_FILE}" | tail -n1 | cut -d= -f2 | tr -d '[:space:]')"
382
- else
383
- echo "MERIDIAN_UI_PORT=${UI_PORT}" >> "${ENV_FILE}"
384
- fi
385
- ok "config at ${ENV_FILE} (dashboard port ${UI_PORT})"
302
+ ok "config at ${ENV_FILE}"
386
303
 
387
304
  # Interactive tracker-credential walkthrough — parity with install.sh. A fresh
388
305
  # `meridian setup` collects Jira/GitHub/Linear keys here; `meridian update`
@@ -552,7 +469,7 @@ if [[ "${SKIP_PERMISSIONS}" -eq 0 ]]; then
552
469
  open "x-apple.systempreferences:com.apple.Notifications-Settings.extension" 2>/dev/null || true
553
470
  echo " → Scroll to the bottom and turn ON"
554
471
  echo " 'Allow notifications when mirroring or sharing the display'."
555
- echo " → When 'Meridian Tray' appears, ensure its notifications are allowed"
472
+ echo " → When 'Meridian' appears, ensure its notifications are allowed"
556
473
  echo " (style Banners or Alerts, not None)."
557
474
  read -r -p " Press Enter when done… " _ || true
558
475
  fi
@@ -561,30 +478,10 @@ fi
561
478
  # this, screenpipe falls back to OCR for those editors instead of their a11y tree.
562
479
  configure_editor_accessibility
563
480
 
564
- # ── 5b. Unpack the dashboard (Turbopack standalone, shipped as a tarball) ─────
565
- # The UI ships as ui.tar.gz rather than an expanded ui/ dir so that Turbopack's
566
- # relative symlinks under .next/node_modules (serverExternalPackages: better-
567
- # sqlite3, pino, @opentelemetry/*) survive `npm publish`, which strips symlinks.
568
- # When meridian-npm-setup.sh detected the tarball hash was unchanged it preserved
569
- # the existing ui/ dir and deleted ui.tar.gz — in that case skip extraction too.
570
- _ui_changed=1
571
- _new_ui_hash=""
572
- if [[ -f "${APP_ROOT}/ui.tar.gz" ]]; then
573
- _new_ui_hash="$(shasum -a 256 "${APP_ROOT}/ui.tar.gz" | cut -d' ' -f1)"
574
- info "Unpacking dashboard…"
575
- rm -rf "${APP_ROOT}/ui"
576
- mkdir -p "${APP_ROOT}/ui"
577
- tar -xzf "${APP_ROOT}/ui.tar.gz" -C "${APP_ROOT}/ui"
578
- rm -f "${APP_ROOT}/ui.tar.gz"
579
- ok "dashboard unpacked ($(find "${APP_ROOT}/ui/.next/node_modules" -type l 2>/dev/null | wc -l | tr -d ' ') external symlink(s) restored)"
580
- elif [[ -d "${APP_ROOT}/ui" ]]; then
581
- # ui/ was preserved by meridian-npm-setup.sh — hash matched, no re-extraction needed
582
- ok "dashboard unchanged — reusing existing build"
583
- _ui_changed=0
584
- else
585
- err "Dashboard bundle missing from ${APP_ROOT} — re-run the installer: curl -fsSL https://raw.githubusercontent.com/Meridiona/meridian/main/scripts/bootstrap.sh | bash"
586
- exit 1
587
- fi
481
+ # ── 5b. Retire the legacy dashboard Node server (one-time, for pre-fold installs)
482
+ # The dashboard is now embedded in the tray binary, so an old standalone UI agent
483
+ # is a zombie. This runs on every (re)install/update — idempotent + non-fatal.
484
+ retire_legacy_ui_server
588
485
 
589
486
  # ── 6. Daemons — restart only what changed ───────────────────────────────────
590
487
  # screenpipe: external npm binary, plist may have changed → always refresh.
@@ -679,7 +576,7 @@ if [[ -x "${APP_ROOT}/bin/meridian-tray" ]]; then
679
576
  if [[ "${_tray_changed}" -eq 0 ]]; then
680
577
  ok "Tray app unchanged — skipping restart"
681
578
  else
682
- info "Installing Meridian Tray launchd agent…"
579
+ info "Installing Meridian tray agent…"
683
580
  bash "${APP_ROOT}/scripts/install-tray-daemon.sh" || warn "tray agent install failed"
684
581
  fi
685
582
  else
@@ -716,31 +613,16 @@ if command -v cursor >/dev/null 2>&1 || [[ -d "${HOME}/Library/Application Suppo
716
613
  else
717
614
  info " Cursor detected but the cursor-agent CLI is missing — Cursor summaries will use the local model (MLX)."
718
615
  info " To summarise with Cursor's own CLI: curl https://cursor.com/install -fsS | bash then: cursor-agent login"
719
- info " Or let the daemon install it on demand: add CURSOR_AGENT_AUTO_INSTALL=1 to ${HOME}/.meridian/app/.env"
616
+ info " Or let the daemon install it on demand: add CURSOR_AGENT_AUTO_INSTALL=1 to ${HOME}/.meridian/.env"
720
617
  fi
721
618
  fi
722
619
 
723
- # UI: even when the build is unchanged we ALWAYS re-pin the Node runtime and
724
- # reload the launchd job. The better-sqlite3 addon is ABI-locked to one Node
725
- # version; skipping this reload is how a stale UI job survives `update` and ends
726
- # up running a mismatched system Node (NODE_MODULE_VERSION crash-loop). The
727
- # expensive re-extraction is already skipped above when unchanged, so this is
728
- # cheap — resolve_node_runtime hits the cache, then bootout/bootstrap reloads.
729
- if [[ "${_ui_changed}" -eq 0 ]]; then
730
- info "Dashboard build unchanged — re-pinning Node runtime and reloading UI agent…"
731
- else
732
- info "Installing the dashboard (UI) launchd agent…"
733
- fi
734
- install_ui_standalone
735
-
736
620
  # Persist component hashes for the next update's differential check.
737
621
  # Write to a temp file and rename atomically so a crash mid-write never leaves
738
622
  # a half-written or empty hash file (which would force a full reinstall).
739
- _final_ui_hash="${_new_ui_hash:-${_OLD_UI_HASH}}"
740
623
  _final_tray_hash="${_new_tray_hash:-${_OLD_TRAY_HASH}}"
741
624
  {
742
625
  [[ -n "${_new_daemon_hash}" ]] && printf 'daemon_bin=%s\n' "${_new_daemon_hash}"
743
- [[ -n "${_final_ui_hash}" ]] && printf 'ui_tarball=%s\n' "${_final_ui_hash}"
744
626
  [[ -n "${_final_tray_hash}" ]] && printf 'tray_bin=%s\n' "${_final_tray_hash}"
745
627
  } > "${_HASH_FILE}.tmp" && mv "${_HASH_FILE}.tmp" "${_HASH_FILE}"
746
628
 
@@ -771,7 +653,7 @@ echo "✓ Meridian installed at ${APP_ROOT}"
771
653
  echo " meridian status # check the daemons"
772
654
  echo " meridian logs -f # watch the pipeline"
773
655
  echo " meridian config edit # add Jira creds"
774
- echo " open http://localhost:${UI_PORT} # the dashboard"
656
+ echo " open the Meridian tray icon in the menu bar → Open Dashboard"
775
657
  echo ""
776
658
  echo "Jira worklogs are DRAFTED only — approve them in the dashboard (Worklogs"
777
659
  echo "view) and the daemon posts approved worklogs within ~60s."
@@ -11,11 +11,12 @@ REPO_ROOT="$(cd "$(dirname "$SELF")/.." && pwd)"
11
11
  # --- constants ---
12
12
  LABEL_SCREENPIPE="com.meridiona.screenpipe"
13
13
  LABEL_DAEMON="com.meridiona.daemon"
14
- LABEL_UI="com.meridiona.ui"
14
+ LABEL_UI="com.meridiona.ui" # retired (dashboard now embedded in the tray) — kept only to boot out a leftover legacy agent
15
15
  LABEL_MLX="com.meridiona.mlx-server"
16
- # Jira worklogs and coding-agent ingest run inside the Rust daemon no
17
- # separate launchd agents. Only these four are managed.
18
- readonly LABELS=("${LABEL_SCREENPIPE}" "${LABEL_DAEMON}" "${LABEL_UI}" "${LABEL_MLX}")
16
+ # Jira worklogs and coding-agent ingest run inside the Rust daemon, and the
17
+ # dashboard is embedded in the tray binary — no separate UI launchd agent. Only
18
+ # these three production services are managed.
19
+ readonly LABELS=("${LABEL_SCREENPIPE}" "${LABEL_DAEMON}" "${LABEL_MLX}")
19
20
  GUI_TARGET="gui/$(id -u)"
20
21
  LAUNCH_AGENTS="${HOME}/Library/LaunchAgents"
21
22
  LOG_DIR="${HOME}/.meridian/logs"
@@ -268,12 +269,11 @@ _doctor_fallback() {
268
269
  printf " ════════════════════════════════════════════════════════\n"
269
270
  _group "system"
270
271
  _row "$([[ "$(uname -s)" == "Darwin" ]] && echo ok || echo fail)" "macOS" ""
271
- _row "$([[ -f "${REPO_ROOT}/.env" ]] && echo ok || echo fail)" "config (.env)" ""
272
+ _row "$([[ -f "${HOME}/.meridian/.env" || -f "${REPO_ROOT}/.env" ]] && echo ok || echo fail)" "config (.env)" ""
272
273
  _group "services (plists)"
273
274
  _plist_row "$LABEL_DAEMON" "daemon plist"
274
275
  _plist_row "$LABEL_SCREENPIPE" "screenpipe plist"
275
276
  _plist_row "$LABEL_MLX" "mlx plist"
276
- _plist_row "$LABEL_UI" "ui plist"
277
277
  _group "builds"
278
278
  _row "$([[ -f "${REPO_ROOT}/packages/meridian-mcp/dist/index.js" ]] && echo ok || echo fail)" "mcp built" ""
279
279
  _row "$([[ -d "${REPO_ROOT}/ui/.next" ]] && echo ok || echo fail)" "ui built" ""
@@ -288,7 +288,9 @@ _doctor_fallback() {
288
288
  # (no flag) full run: classification + worklog synthesis
289
289
 
290
290
  _smoke_read_env() {
291
- local key="$1" env_file="${REPO_ROOT}/.env"
291
+ local key="$1"
292
+ local env_file="${HOME}/.meridian/.env"
293
+ [[ -f "$env_file" ]] || env_file="${REPO_ROOT}/.env"
292
294
  [[ -f "$env_file" ]] || return 0
293
295
  grep -E "^${key}=" "$env_file" 2>/dev/null | tail -1 | cut -d= -f2- || true
294
296
  }
@@ -452,9 +454,10 @@ cmd_config() {
452
454
  err "usage: meridian config edit"
453
455
  exit 1
454
456
  fi
455
- local env_file="${REPO_ROOT}/.env"
457
+ local env_file="${HOME}/.meridian/.env"
458
+ [[ -f "$env_file" ]] || env_file="${REPO_ROOT}/.env"
456
459
  if [[ ! -f "$env_file" ]]; then
457
- err "${env_file} not found — run ./install.sh first"
460
+ err "~/.meridian/.env not found — run the installer first"
458
461
  exit 1
459
462
  fi
460
463
  "${EDITOR:-nano}" "$env_file"
@@ -494,7 +497,9 @@ cmd_uninstall() {
494
497
  set +e
495
498
 
496
499
  # 1. Stop and remove all launchd agents
497
- bash "${REPO_ROOT}/scripts/uninstall-ui-daemon.sh" 2>/dev/null
500
+ # Legacy UI server (retired — dashboard is now in the tray): boot out + remove inline.
501
+ launchctl bootout "${GUI_TARGET}/${LABEL_UI}" 2>/dev/null || true
502
+ rm -f "${HOME}/Library/LaunchAgents/${LABEL_UI}.plist" 2>/dev/null || true
498
503
  bash "${REPO_ROOT}/services/scripts/uninstall-mlx-server-daemon.sh" 2>/dev/null
499
504
  bash "${REPO_ROOT}/scripts/uninstall-daemon.sh" 2>/dev/null
500
505
  bash "${REPO_ROOT}/scripts/uninstall-screenpipe-daemon.sh" 2>/dev/null
@@ -43,13 +43,22 @@ clone_dir() { # <src> <dst>
43
43
  }
44
44
 
45
45
  mkdir -p "${STAGE}"
46
- # Copy the prebuilt payload (bin/ ui.tar.gz services/ scripts/ .env.example VERSION).
46
+ # Copy the prebuilt payload (bin/ services/ scripts/ .env.example VERSION).
47
47
  cp -R "${BUNDLE}/." "${STAGE}/"
48
48
  # Drop npm-package metadata that isn't part of the app.
49
49
  rm -f "${STAGE}/package.json" "${STAGE}/README.md" "${STAGE}/.gitignore" "${STAGE}/.npmignore"
50
50
 
51
- # Preserve an existing .env across re-installs/updates.
52
- [[ -f "${APP}/.env" ]] && cp "${APP}/.env" "${STAGE}/.env"
51
+ # One-time migration: move credentials from the old app/.env location to the
52
+ # canonical ~/.meridian/.env (outside the swap area — untouched by updates).
53
+ # If both exist, the canonical wins and the old copy is removed.
54
+ if [[ -f "${APP}/.env" ]]; then
55
+ if [[ ! -f "${HOME}/.meridian/.env" ]]; then
56
+ mv "${APP}/.env" "${HOME}/.meridian/.env"
57
+ echo "migrated credentials: ~/.meridian/app/.env → ~/.meridian/.env"
58
+ else
59
+ rm -f "${APP}/.env"
60
+ fi
61
+ fi
53
62
 
54
63
  # Preserve the Python venv across updates. The venv is built from PyPI via
55
64
  # uv sync at install time; preserving it means install-from-bundle.sh only
@@ -60,35 +69,9 @@ if [[ -d "${APP}/services/.venv" ]]; then
60
69
  clone_dir "${APP}/services/.venv" "${STAGE}/services/.venv"
61
70
  fi
62
71
 
63
- # UI. Two cases, mirroring install-from-bundle.sh's contract:
64
- # * unchanged (tarball hash matches the recorded one, or no tarball shipped):
65
- # clone the existing ui/ and drop the tarball the installer reads
66
- # "no tarball + ui/ present" as unchanged and skips re-extraction.
67
- # * changed: pre-extract the tarball into staging AND keep the tarball. The
68
- # stage is then complete BEFORE the swap, so an installer crash after the
69
- # swap still leaves a runnable dashboard; the installer's own extraction
70
- # (which records the new hash) re-does only a few seconds of work.
71
- _hash_file="${HOME}/.meridian/.component-hashes"
72
- _ui_preserved=0
73
- if [[ -d "${APP}/ui" ]]; then
74
- if [[ -f "${STAGE}/ui.tar.gz" ]] && [[ -f "${_hash_file}" ]]; then
75
- _old_ui_hash="$(grep '^ui_tarball=' "${_hash_file}" 2>/dev/null | cut -d= -f2 || true)"
76
- _new_ui_hash="$(shasum -a 256 "${STAGE}/ui.tar.gz" | cut -d' ' -f1)"
77
- if [[ -n "${_old_ui_hash}" && "${_new_ui_hash}" == "${_old_ui_hash}" ]]; then
78
- clone_dir "${APP}/ui" "${STAGE}/ui"
79
- rm -f "${STAGE}/ui.tar.gz"
80
- _ui_preserved=1
81
- fi
82
- elif [[ ! -f "${STAGE}/ui.tar.gz" ]]; then
83
- # No tarball in bundle = UI unchanged since last release; keep existing build.
84
- clone_dir "${APP}/ui" "${STAGE}/ui"
85
- _ui_preserved=1
86
- fi
87
- fi
88
- if [[ "${_ui_preserved}" -eq 0 && -f "${STAGE}/ui.tar.gz" ]]; then
89
- mkdir -p "${STAGE}/ui"
90
- tar -xzf "${STAGE}/ui.tar.gz" -C "${STAGE}/ui"
91
- fi
72
+ # The dashboard is no longer staged here: it's embedded in the tray binary, so
73
+ # the bundle ships neither ui.tar.gz nor a ui/ dir. Any old ~/.meridian/app/ui
74
+ # from a pre-fold install is discarded by the swap below (APP OLD → rm).
92
75
 
93
76
  # The swap: two renames on one filesystem. Running daemons keep their open
94
77
  # inodes from the old tree until they restart; the installer (exec'd next)
@@ -101,6 +101,31 @@ _DEFAULT_MLX_MODEL_MIN_RAM_GB = 6.5
101
101
  # runtime by llm_selector.select_mlx_model_id() based on available compute.
102
102
  _MLX_MODEL_ID_PIN = os.environ.get("MLX_MODEL_ID")
103
103
 
104
+ # `~/.meridian/settings.json` — the SAME file the Rust daemon + tray read/write.
105
+ # We read only `llm_model_preference` here: the HuggingFace repo id the user
106
+ # chose in the setup wizard (written by the tray's `set_model_preference`).
107
+ _SETTINGS_PATH = Path(
108
+ os.environ.get("MERIDIAN_SETTINGS_PATH")
109
+ or (Path.home() / ".meridian" / "settings.json")
110
+ )
111
+
112
+
113
+ def _settings_model_preference() -> "str | None":
114
+ """Return the user's chosen model HF id from settings.json, or None.
115
+
116
+ A blank/absent value means "no preference" → fall through to dynamic
117
+ selection. Never raises — a malformed or missing file is treated as unset.
118
+ """
119
+ try:
120
+ with _SETTINGS_PATH.open(encoding="utf-8") as fh:
121
+ data = json.load(fh)
122
+ except (OSError, ValueError):
123
+ return None
124
+ if not isinstance(data, dict):
125
+ return None
126
+ pref = data.get("llm_model_preference")
127
+ return pref if isinstance(pref, str) and pref.strip() else None
128
+
104
129
  # Resolved lazily and cached for the process lifetime by _resolve_model_id().
105
130
  # Kept as a module attribute (not just a function return) so /info, /v1/models,
106
131
  # and the llm_inference span all report the same, truthful id.
@@ -127,12 +152,23 @@ def _resolve_model_id() -> str:
127
152
  from agents.llm_selector import (
128
153
  APPLE_INTELLIGENCE_ID, resolve_model, select_mlx_model_id,
129
154
  )
130
- entry = resolve_model(_DEFAULT_MLX_MODEL_ID)
155
+ # The user's wizard choice (if any) becomes the preferred model; otherwise
156
+ # fall back to the eval-tuned default. Either way `select_mlx_model_id`
157
+ # keeps the preference when Metal budget allows and degrades when it can't,
158
+ # so an over-ambitious choice on a small Mac never OOMs.
159
+ user_pref = _settings_model_preference()
160
+ preferred_id = user_pref or _DEFAULT_MLX_MODEL_ID
161
+ if user_pref:
162
+ log.info(
163
+ "run_task_linker_mlx: model preference from settings.json",
164
+ extra={"model_preference": user_pref},
165
+ )
166
+ entry = resolve_model(preferred_id)
131
167
  preferred_min_ram = (
132
168
  entry["min_ram_gb"] if entry else _DEFAULT_MLX_MODEL_MIN_RAM_GB
133
169
  )
134
170
  selected = select_mlx_model_id(
135
- preferred_hf_id=_DEFAULT_MLX_MODEL_ID,
171
+ preferred_hf_id=preferred_id,
136
172
  preferred_min_ram_gb=preferred_min_ram,
137
173
  )
138
174
  # Propagate the Apple Intelligence sentinel as-is; fall back to the
@@ -17,6 +17,7 @@ import argparse
17
17
  import logging
18
18
  import os
19
19
  import sqlite3 as _sqlite3
20
+ import threading
20
21
  from contextlib import asynccontextmanager
21
22
  from pathlib import Path
22
23
  from typing import Any, AsyncIterator
@@ -150,6 +151,151 @@ async def info() -> dict:
150
151
  }
151
152
 
152
153
 
154
+ # ---------------------------------------------------------------------------
155
+ # Model prefetch — eager, spec-aware download for the onboarding wizard
156
+ # ---------------------------------------------------------------------------
157
+ #
158
+ # The wizard's Model step calls /prefetch_model right after the runtime is
159
+ # provisioned so the ~7 GB classifier weights are on disk before the first
160
+ # classification — instead of the lazy, progress-less download that otherwise
161
+ # fires mid-inference. The model id is resolved by `_resolve_model_id()`, which
162
+ # is ALREADY spec-aware (picks the best fit for this machine's Metal headroom /
163
+ # RAM / chip via `llm_selector.select_mlx_model_id`), so eager == spec-aware
164
+ # without any manifest change.
165
+ #
166
+ # We download to the HF cache WITHOUT loading weights into Metal memory: the
167
+ # download primitive is `mlx_lm.utils._download(model_id)` — the SAME call (and
168
+ # therefore the same `allow_patterns` fileset) that `mlx_lm.load()` resolves on
169
+ # the non-sharded path — so the first real `load()` finds everything cached and
170
+ # never touches the network. A bare `snapshot_download` could fetch a different
171
+ # set and leave `load()` to re-download silently; we only fall back to it (with
172
+ # the identical pattern list replicated) if the private primitive is missing.
173
+
174
+ # MUST stay in sync with `mlx_lm.utils._download`'s default `allow_patterns`
175
+ # (used by `load()` on the non-sharded path) — the fallback relies on parity.
176
+ _MODEL_ALLOW_PATTERNS = [
177
+ "*.json", "model*.safetensors", "*.py", "tokenizer.model",
178
+ "*.tiktoken", "tiktoken.model", "*.txt", "*.jsonl", "*.jinja",
179
+ ]
180
+
181
+ # Shared prefetch progress, guarded by _prefetch_lock. states: idle|downloading|done|error
182
+ _prefetch_state: dict[str, Any] = {
183
+ "state": "idle", "model_id": None, "received": 0, "total": 0, "error": None,
184
+ }
185
+ _prefetch_lock = threading.Lock()
186
+
187
+
188
+ def _hf_cache_dir_for(model_id: str) -> Path:
189
+ """The HF hub cache directory for `model_id` (where partial + complete blobs land)."""
190
+ from huggingface_hub.constants import HF_HUB_CACHE
191
+ return Path(HF_HUB_CACHE) / ("models--" + model_id.replace("/", "--"))
192
+
193
+
194
+ def _dir_size_bytes(path: Path) -> int:
195
+ """Sum of all file sizes under `path` (includes HF `.incomplete` partials → live progress)."""
196
+ total = 0
197
+ if path.exists():
198
+ for f in path.rglob("*"):
199
+ try:
200
+ if f.is_file():
201
+ total += f.stat().st_size
202
+ except OSError:
203
+ pass
204
+ return total
205
+
206
+
207
+ def _prefetch_total_bytes(model_id: str) -> int:
208
+ """Authoritative download size: sum HF sibling sizes filtered to the load() patterns.
209
+
210
+ Computed upfront so the wizard's progress bar has a stable denominator, instead
211
+ of summing concurrent per-file tqdm totals (which lurch as new bars spawn).
212
+ """
213
+ import fnmatch
214
+ from huggingface_hub import HfApi
215
+ info = HfApi().model_info(model_id, files_metadata=True)
216
+ total = 0
217
+ for sib in info.siblings or []:
218
+ if any(fnmatch.fnmatch(sib.rfilename, pat) for pat in _MODEL_ALLOW_PATTERNS):
219
+ total += sib.size or 0
220
+ return total
221
+
222
+
223
+ def _run_prefetch(model_id: str) -> None:
224
+ """Background worker: download the model's weights to the HF cache (no load)."""
225
+ tracer = trace.get_tracer(__name__)
226
+ with tracer.start_as_current_span("model_prefetch") as span:
227
+ span.set_attribute("model_id", model_id)
228
+ try:
229
+ try:
230
+ from mlx_lm.utils import _download as _mlx_download
231
+ _mlx_download(model_id) # exact fileset load() resolves; download-only
232
+ except (ImportError, AttributeError):
233
+ # Private primitive unavailable — replicate load()'s default patterns.
234
+ from huggingface_hub import snapshot_download
235
+ snapshot_download(model_id, allow_patterns=_MODEL_ALLOW_PATTERNS)
236
+ received = _dir_size_bytes(_hf_cache_dir_for(model_id))
237
+ with _prefetch_lock:
238
+ _prefetch_state["received"] = received or _prefetch_state["total"]
239
+ _prefetch_state["state"] = "done"
240
+ span.set_attribute("received_bytes", received)
241
+ log.info("server: model prefetch complete", extra={"model_id": model_id, "received_bytes": received})
242
+ except Exception as exc: # noqa: BLE001 — report, never crash the server
243
+ with _prefetch_lock:
244
+ _prefetch_state["state"] = "error"
245
+ _prefetch_state["error"] = str(exc)
246
+ span.set_status(trace.Status(trace.StatusCode.ERROR, str(exc)))
247
+ log.error("server: model prefetch failed", extra={"model_id": model_id, "error": str(exc)})
248
+
249
+
250
+ @app.post("/prefetch_model")
251
+ async def prefetch_model() -> dict:
252
+ """Start the eager, spec-aware model download (idempotent). Returns current status.
253
+
254
+ Apple Intelligence backend → nothing to download (no-op `done`). Re-POSTing
255
+ while `downloading`/`done` returns the live state without spawning a second
256
+ download; an earlier `error` is retried.
257
+ """
258
+ from fastapi.concurrency import run_in_threadpool
259
+
260
+ from agents.llm_selector import APPLE_INTELLIGENCE_ID
261
+ m = _app_state.get("mlx_module")
262
+ model_id = m._resolve_model_id() if m else None
263
+ if model_id == APPLE_INTELLIGENCE_ID or model_id is None:
264
+ return {"state": "done", "model_id": model_id, "received": 0, "total": 0, "error": None}
265
+
266
+ with _prefetch_lock:
267
+ # Idempotent only for the SAME model: a completed/in-flight prefetch for
268
+ # one model must not block starting a different one after the user changes
269
+ # their model preference (which changes what _resolve_model_id() returns).
270
+ same_model = _prefetch_state.get("model_id") == model_id
271
+ if same_model and _prefetch_state["state"] in ("downloading", "done"):
272
+ return dict(_prefetch_state) # idempotent — no duplicate downloads
273
+ _prefetch_state.update(state="downloading", model_id=model_id, received=0, total=0, error=None)
274
+
275
+ try:
276
+ total = await run_in_threadpool(_prefetch_total_bytes, model_id)
277
+ except Exception as exc: # noqa: BLE001 — size probe is best-effort; download still runs
278
+ total = 0
279
+ log.warning("server: prefetch size-probe failed (bar will be indeterminate)", extra={"error": str(exc)})
280
+ with _prefetch_lock:
281
+ _prefetch_state["total"] = total
282
+
283
+ threading.Thread(target=_run_prefetch, args=(model_id,), daemon=True).start()
284
+ log.info("server: model prefetch started", extra={"model_id": model_id, "total_bytes": total})
285
+ with _prefetch_lock:
286
+ return dict(_prefetch_state)
287
+
288
+
289
+ @app.get("/prefetch_status")
290
+ async def prefetch_status() -> dict:
291
+ """Live prefetch progress. `received` is recomputed from the cache dir while downloading."""
292
+ with _prefetch_lock:
293
+ st = dict(_prefetch_state)
294
+ if st["state"] == "downloading" and st["model_id"]:
295
+ st["received"] = _dir_size_bytes(_hf_cache_dir_for(st["model_id"]))
296
+ return st
297
+
298
+
153
299
  # ---------------------------------------------------------------------------
154
300
  # MLX backend — direct in-process inference, model pre-loaded at startup
155
301
  # ---------------------------------------------------------------------------
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meridian-agents"
7
- version = "1.63.0"
7
+ version = "1.64.0"
8
8
  description = "Meridian agents — MLX classifier server and Jira worklog synthesis for meridian.db"
9
9
  requires-python = ">=3.11"
10
10
  authors = [{ name = "Meridiona" }]
package/services/uv.lock CHANGED
@@ -1196,7 +1196,7 @@ wheels = [
1196
1196
 
1197
1197
  [[package]]
1198
1198
  name = "meridian-agents"
1199
- version = "1.48.2"
1199
+ version = "1.56.0"
1200
1200
  source = { editable = "." }
1201
1201
  dependencies = [
1202
1202
  { name = "mcp" },
@@ -1,2 +0,0 @@
1
- NODE_RUNTIME_VERSION=22.22.3
2
- NODE_RUNTIME_SHA=0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207
@@ -1,62 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!--
3
- com.meridiona.ui — runs the Next.js dashboard (built by ./install.sh) as
4
- a background launchd LaunchAgent. Serves on http://localhost:3939.
5
-
6
- Install via scripts/install-ui-daemon.sh which copies this file into
7
- ~/Library/LaunchAgents/ with absolute paths substituted in, then
8
- bootstraps it under launchd. The plist below uses {{HOME}}, {{REPO_ROOT}}
9
- and {{NPM_BIN}} placeholders that the install script replaces.
10
-
11
- Uninstall:
12
- scripts/uninstall-ui-daemon.sh
13
- Or manually:
14
- launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.meridiona.ui.plist
15
- rm ~/Library/LaunchAgents/com.meridiona.ui.plist
16
- -->
17
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
18
- <plist version="1.0">
19
- <dict>
20
- <key>Label</key>
21
- <string>com.meridiona.ui</string>
22
-
23
- <key>WorkingDirectory</key>
24
- <string>{{REPO_ROOT}}/ui</string>
25
-
26
- <key>ProgramArguments</key>
27
- <array>
28
- <string>{{NPM_BIN}}</string>
29
- <string>run</string>
30
- <string>start</string>
31
- </array>
32
-
33
- <key>EnvironmentVariables</key>
34
- <dict>
35
- <key>PATH</key>
36
- <string>{{NODE_BIN_DIR}}/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
37
- <key>HOME</key>
38
- <string>{{HOME}}</string>
39
- <key>MERIDIAN_DB</key>
40
- <string>{{HOME}}/.meridian/meridian.db</string>
41
- <key>PORT</key>
42
- <string>3939</string>
43
- <key>NODE_ENV</key>
44
- <string>production</string>
45
- </dict>
46
-
47
- <key>StandardOutPath</key>
48
- <string>{{HOME}}/.meridian/logs/ui.log</string>
49
- <key>StandardErrorPath</key>
50
- <string>{{HOME}}/.meridian/logs/ui-error.log</string>
51
-
52
- <!-- RunAtLoad true: dashboard should always be available after login. -->
53
- <key>RunAtLoad</key>
54
- <true/>
55
- <key>KeepAlive</key>
56
- <true/>
57
- <key>ThrottleInterval</key>
58
- <integer>30</integer>
59
- <key>ProcessType</key>
60
- <string>Background</string>
61
- </dict>
62
- </plist>
@@ -1,95 +0,0 @@
1
- #!/usr/bin/env bash
2
- # ambient dev tool that watches what you do and updates your PM tickets automatically, boosting developer productivity
3
- # Install the meridian Next.js dashboard as a launchd LaunchAgent under the
4
- # current user. Serves on http://localhost:3939. Built artifact must exist
5
- # at ui/.next/ before this script runs (install.sh handles that).
6
- #
7
- # ./scripts/install-ui-daemon.sh
8
- #
9
- # Re-running this script is safe — it bootouts the existing agent first,
10
- # rewrites the plist with current paths, and reloads it.
11
- #
12
- # Uninstall:
13
- # ./scripts/uninstall-ui-daemon.sh
14
-
15
- set -euo pipefail
16
-
17
- LABEL="com.meridiona.ui"
18
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
- REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
20
- TEMPLATE="${SCRIPT_DIR}/${LABEL}.plist"
21
-
22
- LAUNCH_AGENTS="${HOME}/Library/LaunchAgents"
23
- PLIST_DEST="${LAUNCH_AGENTS}/${LABEL}.plist"
24
-
25
- GUI_TARGET="gui/$(id -u)"
26
-
27
- if [[ ! -f "${TEMPLATE}" ]]; then
28
- echo "✗ template not found: ${TEMPLATE}" >&2
29
- exit 1
30
- fi
31
-
32
- NPM_BIN="$(command -v npm 2>/dev/null || true)"
33
- if [[ -z "${NPM_BIN}" ]]; then
34
- echo "✗ npm not found in PATH — install Node.js 18+ and re-run" >&2
35
- exit 1
36
- fi
37
- # Capture the node binary directory so launchd's restricted PATH includes it.
38
- # `npm run start` invokes `next start` whose shebang resolves `node` via PATH;
39
- # NVM / fnm / asdf install node outside /opt/homebrew and /usr/local, so the
40
- # standard launchd PATH misses it. Injecting NODE_BIN_DIR fixes this.
41
- NODE_BIN="$(command -v node 2>/dev/null || true)"
42
- NODE_BIN_DIR_PREFIX=""
43
- if [[ -n "${NODE_BIN}" ]]; then
44
- NODE_BIN_DIR_PREFIX="$(dirname "${NODE_BIN}"):"
45
- fi
46
-
47
- if [[ ! -d "${REPO_ROOT}/ui/.next" ]]; then
48
- echo "✗ ui/.next not found — run \`cd ui && npm ci && npm run build\` first" >&2
49
- echo " (or just run ./install.sh which does it for you)" >&2
50
- exit 1
51
- fi
52
-
53
- mkdir -p "${HOME}/.meridian/logs"
54
- mkdir -p "${LAUNCH_AGENTS}"
55
-
56
- echo "→ writing ${PLIST_DEST}"
57
- sed \
58
- -e "s|{{REPO_ROOT}}|${REPO_ROOT}|g" \
59
- -e "s|{{HOME}}|${HOME}|g" \
60
- -e "s|{{NPM_BIN}}|${NPM_BIN}|g" \
61
- -e "s|{{NODE_BIN_DIR}}|${NODE_BIN_DIR_PREFIX}|g" \
62
- "${TEMPLATE}" > "${PLIST_DEST}"
63
-
64
- if ! plutil -lint "${PLIST_DEST}" >/dev/null; then
65
- echo "✗ plist failed validation" >&2
66
- exit 1
67
- fi
68
-
69
- echo "→ bootout ${LABEL} (if loaded)"
70
- launchctl bootout "${GUI_TARGET}/${LABEL}" 2>/dev/null || true
71
- # bootout is async — wait until the domain entry actually clears before
72
- # bootstrapping, otherwise launchctl bootstrap can fail with EIO (errno 5).
73
- _bootout_wait=0
74
- while launchctl print "${GUI_TARGET}/${LABEL}" >/dev/null 2>&1; do
75
- sleep 1
76
- _bootout_wait=$(( _bootout_wait + 1 ))
77
- if [[ "${_bootout_wait}" -ge 15 ]]; then
78
- echo "⚠ ${LABEL} still in launchd domain after 15s — proceeding anyway" >&2
79
- break
80
- fi
81
- done
82
-
83
- echo "→ bootstrap ${LABEL}"
84
- launchctl enable "${GUI_TARGET}/${LABEL}" 2>/dev/null || true
85
- launchctl bootstrap "${GUI_TARGET}" "${PLIST_DEST}"
86
- launchctl enable "${GUI_TARGET}/${LABEL}"
87
- launchctl kickstart -k "${GUI_TARGET}/${LABEL}"
88
-
89
- echo
90
- echo "✓ UI installed and started"
91
- echo
92
- echo " open http://localhost:3939 # the dashboard"
93
- echo " tail -f ~/.meridian/logs/ui.log # live stdout"
94
- echo " tail -f ~/.meridian/logs/ui-error.log # live stderr"
95
- echo " ${SCRIPT_DIR}/uninstall-ui-daemon.sh # remove"
@@ -1,68 +0,0 @@
1
- #!/usr/bin/env bash
2
- # ambient dev tool that watches what you do and updates your PM tickets automatically, boosting developer productivity
3
- #
4
- # Startup wrapper for the Next.js UI daemon.
5
- #
6
- # The dashboard's better-sqlite3 native addon is ABI-locked to ONE exact Node
7
- # version — the one CI built it against, recorded in bin/node-runtime.meta as
8
- # NODE_RUNTIME_VERSION. Running any other Node major triggers a
9
- # NODE_MODULE_VERSION (ABI) mismatch and the dashboard crash-loops. So on a
10
- # bundle install we resolve the cached, version-matched runtime under
11
- # ~/.meridian/node-runtime ourselves — we do NOT trust a stale MERIDIAN_NODE_BIN
12
- # or system node that a `meridian update` may have left behind (that is exactly
13
- # how the UI ended up on a mismatched Node). If the matched runtime is missing we
14
- # fail LOUD with remediation rather than silently crash-looping under the wrong
15
- # Node. (A source/dev install has no meta file: there better-sqlite3 was compiled
16
- # against the local node, so we prefer MERIDIAN_NODE_BIN then system node.)
17
- set -euo pipefail
18
-
19
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
- APP_ROOT="$(dirname "${SCRIPT_DIR}")"
21
- META="${APP_ROOT}/bin/node-runtime.meta"
22
-
23
- log() { echo "[meridian-ui] $*" >&2; }
24
-
25
- # `node -v` → bare version (strip the leading "v"); empty if it can't run.
26
- node_version() { "${1}" -v 2>/dev/null | sed 's/^v//'; }
27
-
28
- # The exact Node version the shipped addon was built against (bundle install
29
- # only). `|| true` so a malformed/lineless meta doesn't trip `set -e`.
30
- REQUIRED_VER=""
31
- if [[ -f "${META}" ]]; then
32
- REQUIRED_VER="$(grep '^NODE_RUNTIME_VERSION=' "${META}" 2>/dev/null | cut -d= -f2 | tr -d '[:space:]' || true)"
33
- fi
34
-
35
- NODE=""
36
- if [[ -n "${REQUIRED_VER}" ]]; then
37
- # Bundle install: enforce the ABI-matched runtime. The cached runtime that
38
- # install-from-bundle.sh downloaded for this exact version is the source of
39
- # truth; accept MERIDIAN_NODE_BIN only if it actually IS that version.
40
- cached="${HOME}/.meridian/node-runtime/v${REQUIRED_VER}/bin/node"
41
- if [[ -x "${cached}" ]] && [[ "$(node_version "${cached}")" == "${REQUIRED_VER}" ]]; then
42
- NODE="${cached}"
43
- elif [[ -n "${MERIDIAN_NODE_BIN:-}" ]] && [[ -x "${MERIDIAN_NODE_BIN}" ]] \
44
- && [[ "$(node_version "${MERIDIAN_NODE_BIN}")" == "${REQUIRED_VER}" ]]; then
45
- NODE="${MERIDIAN_NODE_BIN}"
46
- else
47
- log "dashboard requires Node ${REQUIRED_VER} (better-sqlite3 ABI), but the"
48
- log "cached runtime at ${cached} is missing or wrong-versioned."
49
- log "Fetch it: run 'meridian update' with a connection, or reinstall:"
50
- log " curl -fsSL https://raw.githubusercontent.com/Meridiona/meridian/main/scripts/bootstrap.sh | bash"
51
- # EX_CONFIG: refuse to start under a mismatched Node. KeepAlive will retry,
52
- # but the log now states the exact cause instead of a cryptic ERR_DLOPEN.
53
- exit 78
54
- fi
55
- else
56
- # Source/dev install (no meta): better-sqlite3 was compiled against the local
57
- # node, so prefer MERIDIAN_NODE_BIN then system node — ABI matches by build.
58
- # launchd agents don't inherit the user's PATH, so probe known locations.
59
- NODE="${MERIDIAN_NODE_BIN:-}"
60
- if [[ -z "${NODE}" ]] || [[ ! -x "${NODE}" ]]; then
61
- for _n in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
62
- if [[ -x "${_n}" ]]; then NODE="${_n}"; break; fi
63
- done
64
- fi
65
- [[ -x "${NODE:-}" ]] || { log "node not found — cannot start UI server"; exit 1; }
66
- fi
67
-
68
- exec "${NODE}" "${APP_ROOT}/ui/server.js"
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env bash
2
- # ambient dev tool that watches what you do and updates your PM tickets automatically, boosting developer productivity
3
- # Stop and remove the meridian UI launchd agent.
4
-
5
- set -euo pipefail
6
-
7
- LABEL="com.meridiona.ui"
8
- PLIST="${HOME}/Library/LaunchAgents/${LABEL}.plist"
9
- GUI_TARGET="gui/$(id -u)"
10
-
11
- if [[ ! -f "${PLIST}" ]]; then
12
- echo "(${LABEL} not installed)"
13
- exit 0
14
- fi
15
-
16
- if launchctl print "${GUI_TARGET}/${LABEL}" >/dev/null 2>&1; then
17
- echo "→ bootout ${LABEL}"
18
- launchctl bootout "${GUI_TARGET}" "${PLIST}" || true
19
- fi
20
-
21
- rm -f "${PLIST}"
22
- echo "✓ ${LABEL} uninstalled"
package/ui.tar.gz DELETED
Binary file