@meridiona/meridian-darwin-arm64 1.62.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 +1 -1
- package/bin/meridian +0 -0
- package/bin/meridian-tray +0 -0
- package/package.json +1 -1
- package/scripts/install-from-bundle.sh +42 -160
- package/scripts/meridian-cli.sh +15 -10
- package/scripts/meridian-npm-setup.sh +15 -32
- package/services/agents/run_task_linker_mlx.py +38 -2
- package/services/agents/server.py +146 -0
- package/services/pyproject.toml +1 -1
- package/services/uv.lock +1 -1
- package/bin/node-runtime.meta +0 -2
- package/scripts/com.meridiona.ui.plist +0 -62
- package/scripts/install-ui-daemon.sh +0 -95
- package/scripts/ui-start.sh +0 -68
- package/scripts/uninstall-ui-daemon.sh +0 -22
- package/ui.tar.gz +0 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
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.
|
|
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
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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:
|
|
371
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
565
|
-
# The
|
|
566
|
-
#
|
|
567
|
-
|
|
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
|
|
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
|
|
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
|
|
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."
|
package/scripts/meridian-cli.sh
CHANGED
|
@@ -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
|
|
17
|
-
#
|
|
18
|
-
|
|
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"
|
|
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="${
|
|
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 "
|
|
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
|
-
|
|
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/
|
|
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
|
-
#
|
|
52
|
-
|
|
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
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
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
|
-
|
|
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=
|
|
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
|
# ---------------------------------------------------------------------------
|
package/services/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "meridian-agents"
|
|
7
|
-
version = "1.
|
|
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
package/bin/node-runtime.meta
DELETED
|
@@ -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"
|
package/scripts/ui-start.sh
DELETED
|
@@ -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
|