@meridiona/meridian-darwin-arm64 1.63.0 → 1.65.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.
Files changed (95) hide show
  1. package/VERSION +1 -1
  2. package/bin/meridian +0 -0
  3. package/bin/meridian-tray +0 -0
  4. package/package.json +1 -1
  5. package/scripts/install-from-bundle.sh +42 -160
  6. package/scripts/meridian-cli.sh +11 -5
  7. package/scripts/meridian-npm-setup.sh +15 -32
  8. package/services/README.md +6 -12
  9. package/services/agents/README.md +10 -16
  10. package/services/agents/_state.py +34 -0
  11. package/services/agents/agno_viewer.py +60 -0
  12. package/services/agents/config.py +8 -0
  13. package/services/agents/mlx_classifier.py +195 -0
  14. package/services/agents/observability.py +48 -2
  15. package/services/agents/pm_worklog_update/config.py +3 -3
  16. package/services/agents/prompts/__init__.py +0 -0
  17. package/services/agents/prompts/activity_report.py +71 -0
  18. package/services/agents/prompts/coding_agent_session_summary.py +28 -0
  19. package/services/agents/reranker.py +118 -0
  20. package/services/agents/routes/__init__.py +6 -0
  21. package/services/agents/routes/activity.py +162 -0
  22. package/services/agents/routes/chat.py +175 -0
  23. package/services/agents/routes/distill.py +103 -0
  24. package/services/agents/routes/health.py +41 -0
  25. package/services/agents/routes/prefetch.py +138 -0
  26. package/services/agents/routes/rerank.py +79 -0
  27. package/services/agents/routes/summarise.py +131 -0
  28. package/services/agents/routes/worklog.py +103 -0
  29. package/services/agents/server.py +58 -904
  30. package/services/agents/session_distiller.py +537 -0
  31. package/services/agents/worklog_pipeline/__init__.py +11 -0
  32. package/services/agents/worklog_pipeline/agent_io.py +75 -0
  33. package/services/agents/worklog_pipeline/db.py +277 -0
  34. package/services/agents/worklog_pipeline/match.py +145 -0
  35. package/services/agents/worklog_pipeline/models.py +98 -0
  36. package/services/agents/worklog_pipeline/pipeline.py +418 -0
  37. package/services/agents/worklog_pipeline/prompts/__init__.py +1 -0
  38. package/services/agents/worklog_pipeline/prompts/match_tasks.py +38 -0
  39. package/services/agents/worklog_pipeline/prompts/propose_ticket.py +19 -0
  40. package/services/agents/worklog_pipeline/prompts/worklog.py +20 -0
  41. package/services/agents/worklog_pipeline/workflow.py +217 -0
  42. package/services/agents/worklog_pipeline/worklog.py +41 -0
  43. package/services/pyproject.toml +1 -1
  44. package/services/scripts/com.meridiona.mlx-server.plist +3 -3
  45. package/services/scripts/count_classifier_tokens.py +1 -1
  46. package/services/scripts/install-mlx-server-daemon.sh +3 -3
  47. package/services/tests/evals/README.md +7 -7
  48. package/services/tests/evals/configs/baseline_a_meridian.json +3 -3
  49. package/services/tests/evals/configs/baseline_b_generic.json +3 -3
  50. package/services/tests/evals/configs/extract_then_classify_b_generic.json +3 -3
  51. package/services/uv.lock +1 -1
  52. package/bin/node-runtime.meta +0 -2
  53. package/scripts/com.meridiona.ui.plist +0 -62
  54. package/scripts/install-ui-daemon.sh +0 -95
  55. package/scripts/ui-start.sh +0 -68
  56. package/scripts/uninstall-ui-daemon.sh +0 -22
  57. package/services/agents/_parser.py +0 -187
  58. package/services/agents/_prompts.py +0 -238
  59. package/services/agents/_system_context.py +0 -42
  60. package/services/agents/llm_selector.py +0 -728
  61. package/services/agents/run_task_linker_mlx.py +0 -1837
  62. package/services/agents/tests/test_llm_selector.py +0 -183
  63. package/services/agents/tests/test_parser.py +0 -134
  64. package/services/agents/tests/test_run_task_linker_mlx.py +0 -1127
  65. package/services/observability/dashboards/classifier-debug.json +0 -187
  66. package/services/observability/dashboards/pm-worklog-debug.json +0 -140
  67. package/services/skills/activity/task-classifier/FEEDBACK.json +0 -2656
  68. package/services/skills/activity/task-classifier/SKILL.applefm.md +0 -77
  69. package/services/skills/activity/task-classifier/SKILL.md +0 -136
  70. package/services/tests/conftest.py +0 -7
  71. package/services/tests/evals/build_dataset.py +0 -178
  72. package/services/tests/evals/build_real_goldens.py +0 -180
  73. package/services/tests/evals/classify_session.py +0 -126
  74. package/services/tests/evals/compare_pipeline.py +0 -262
  75. package/services/tests/evals/conftest.py +0 -5
  76. package/services/tests/evals/data/labels/real_2026-05-28.json +0 -1172
  77. package/services/tests/evals/data/labels/real_curated.json +0 -166
  78. package/services/tests/evals/data/labels/real_curated_holdout.json +0 -97
  79. package/services/tests/evals/data/labels/real_curated_holdout2.json +0 -64
  80. package/services/tests/evals/data/seeds/sessions_a_meridian.json +0 -1757
  81. package/services/tests/evals/data/seeds/sessions_b_generic.json +0 -1319
  82. package/services/tests/evals/data/seeds/tickets_generic.json +0 -72
  83. package/services/tests/evals/data/seeds/tickets_meridian.json +0 -87
  84. package/services/tests/evals/eval_classifier.py +0 -660
  85. package/services/tests/evals/metrics.py +0 -260
  86. package/services/tests/evals/render_seeds.py +0 -143
  87. package/services/tests/evals/strategies.py +0 -655
  88. package/services/tests/evals/test_classifier.py +0 -343
  89. package/services/tests/evals/test_model_sweep.py +0 -197
  90. package/services/tests/test_continuity_context.py +0 -202
  91. package/services/tests/test_fetch_pm_tasks.py +0 -120
  92. package/services/tests/test_format_candidates.py +0 -45
  93. package/services/tests/test_llm_selector.py +0 -223
  94. package/services/tests/test_prompt_cache_equivalence.py +0 -97
  95. package/ui.tar.gz +0 -0
package/VERSION CHANGED
@@ -1 +1 @@
1
- 1.63.0
1
+ 1.65.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.65.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."
@@ -13,9 +13,10 @@ LABEL_SCREENPIPE="com.meridiona.screenpipe"
13
13
  LABEL_DAEMON="com.meridiona.daemon"
14
14
  LABEL_UI="com.meridiona.ui"
15
15
  LABEL_MLX="com.meridiona.mlx-server"
16
+ # Capture runs in-process in the tray (no screenpipe launchd agent since Bucket-2).
16
17
  # 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}")
18
+ # separate launchd agents. Only these two are managed.
19
+ readonly LABELS=("${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"
@@ -40,7 +41,7 @@ Commands:
40
41
  restart Stop, wait 1s, start
41
42
  status Show running state of all daemons
42
43
  logs [target] Tail log files
43
- target: daemon|daemon-error|screenpipe|screenpipe-error|ui|ui-error|mlx-server|mlx-server-error
44
+ target: daemon|daemon-error|mlx-server|mlx-server-error|tray|tray-error
44
45
  -f Follow (stream)
45
46
  -n N Last N lines (default 100)
46
47
  doctor Run environment health checks (includes pipeline smoke)
@@ -179,7 +180,12 @@ cmd_logs() {
179
180
  ui-error) log_file="${LOG_DIR}/ui-error.log" ;;
180
181
  mlx-server) log_file="${LOG_DIR}/mlx-server.log" ;;
181
182
  mlx-server-error) log_file="${LOG_DIR}/mlx-server-error.log" ;;
182
- *) err "unknown log target: ${target} (daemon|daemon-error|screenpipe|screenpipe-error|ui|ui-error|mlx-server|mlx-server-error)"; exit 1 ;;
183
+ tray) log_file="${LOG_DIR}/tray.log" ;;
184
+ tray-error) log_file="${LOG_DIR}/tray-error.log" ;;
185
+ # screenpipe/ui are retired post-v1.64.0 (capture is in-process in the
186
+ # tray; the dashboard is embedded) — kept here so old log files remain
187
+ # tailable, but `tray`/`tray-error` are the live targets now.
188
+ *) err "unknown log target: ${target} (daemon|daemon-error|mlx-server|mlx-server-error|tray|tray-error)"; exit 1 ;;
183
189
  esac
184
190
 
185
191
  if [[ ! -f "$log_file" ]]; then
@@ -563,7 +569,7 @@ PYEOF
563
569
  "models--mlx-community--phi-4-4bit"
564
570
  "models--mlx-community--DeepSeek-R1-Distill-Qwen-14B-4bit"
565
571
  "models--mlx-community--gemma-3-12b-it-qat-4bit"
566
- "models--mlx-community--Qwen3.5-9B-OptiQ-4bit"
572
+ "models--mlx-community--Qwen3.5-2B-OptiQ-4bit"
567
573
  "models--mlx-community--Qwen3.5-4B-MLX-4bit"
568
574
  "models--mlx-community--Llama-3.2-3B-Instruct-4bit"
569
575
  )
@@ -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)
@@ -4,7 +4,7 @@ The model layer that runs alongside the Rust daemon. A persistent MLX inference
4
4
 
5
5
  The Rust daemon owns all DDL; this service only does SELECT/INSERT/UPDATE on its agent-side tables.
6
6
 
7
- > **Note:** the coding-agent indexer + summariser, and the pm-worklog (Stage 4) driver, all run **inside the Rust daemon** (`src/coding_agent_session_ingest/`, `src/pm_worklog/`). This service is the model layer: `agents/server.py` is the persistent MLX server, which serves `/classify_sessions` (classification), `/summarise` (the summariser's fallback), and `/synthesise_worklog` (the agno worklog synthesiser). The Rust daemon drives all three over HTTP.
7
+ > **Note:** the coding-agent indexer + summariser, and the pm-worklog (Stage 4) driver, all run **inside the Rust daemon** (`src/coding_agent_session_ingest/`, `src/pm_worklog/`). This service is the model layer: `agents/server.py` is the persistent MLX server, which serves `/summarise` (the summariser's fallback) and `/synthesise_worklog` (the agno worklog synthesiser). The Rust daemon drives both over HTTP.
8
8
 
9
9
  For the deep technical reference (classification logic, schema, recipes), see [`agents/README.md`](agents/README.md).
10
10
 
@@ -15,14 +15,8 @@ For the deep technical reference (classification logic, schema, recipes), see [`
15
15
  ```
16
16
  app_sessions row (Rust ETL writes it)
17
17
 
18
- │ Rust daemon calls POST /classify_sessions
19
- ▼ on the persistent MLX server
20
- ┌──────────────────────────────────────────────┐
21
- │ MLX inference server (FastAPI, port 7823) │ Apple Silicon only
22
- │ model: mlx-community/Qwen3.5-9B-OptiQ-4bit│
23
- │ loaded once at startup, served until killed│
24
- │ returns: task_key, session_type, confidence│
25
- └──────────────────────────────────────────────┘
18
+
19
+ Rust daemon classifies sessions (src/intelligence/task_linker/)
26
20
 
27
21
 
28
22
  Rust writes ticket_links + session_dimensions → meridian.db
@@ -50,7 +44,7 @@ The `hermes-agent` package is fetched from GitHub at the pinned tag; an internet
50
44
 
51
45
  ## MLX server
52
46
 
53
- The Rust daemon calls the MLX server for every classification. It must be running before the daemon starts.
47
+ The Rust daemon calls the MLX server for summarisation and worklog synthesis. It must be running before the daemon starts.
54
48
 
55
49
  ### Install as a launchd daemon (recommended)
56
50
 
@@ -74,7 +68,7 @@ RUST_LOG=meridian=debug CLASSIFIER_BACKEND=mlx cargo run --bin meridian
74
68
  python -m agents.server --backend mlx --port 7823
75
69
  ```
76
70
 
77
- The model (`Qwen3.5-9B-OptiQ-4bit`) is downloaded from Hugging Face on first run (~4 GB). Subsequent starts load from local cache in ~5 s.
71
+ The model (`Qwen3.5-2B-OptiQ-4bit`) is downloaded from Hugging Face on first run. Subsequent starts load from local cache in ~5 s.
78
72
 
79
73
  ---
80
74
 
@@ -96,7 +90,7 @@ Additional variables are documented in [`agents/README.md`](agents/README.md#con
96
90
 
97
91
  ### Task classifier
98
92
 
99
- The task classifier runs automatically the Rust daemon calls `POST /classify_sessions` on the MLX server on each intelligence tick. To inspect or re-run classification:
93
+ The task classifier runs automatically inside the Rust daemon on each intelligence tick. To inspect or re-run classification:
100
94
 
101
95
  ```bash
102
96
  # Re-run classification with full logging — does NOT write to DB
@@ -11,7 +11,7 @@ For a higher-level overview (installation, daemon ops, configuration), see [`ser
11
11
  The Rust daemon owns the pipeline (ETL, coding-agent ingest, classification
12
12
  trigger, pm-worklog driver) and the database. This service is the **model
13
13
  layer**: one persistent MLX server (`server.py`) that the daemon calls over HTTP
14
- for the three jobs that need a local LLM.
14
+ for the two jobs that need a local LLM.
15
15
 
16
16
  ```
17
17
  screenpipe.db (read-only)
@@ -29,16 +29,10 @@ meridian.db → app_sessions
29
29
  │ MLX server's /summarise is the fallback. Rows walk task_method:
30
30
  │ coding_agent_live → pending_summariser → pending_classifier.
31
31
 
32
- ├── classification (src/intelligence/task_linker/, Rust → MLX)
32
+ ├── classification (src/intelligence/task_linker/, Rust)
33
33
  │ Rust reads unclassified rows and the pending_classifier queue,
34
- then sends:
35
- POST http://127.0.0.1:7823/classify_sessions
36
- │ {session_ids: [...], meridian_db: ...}
37
- │ MLX server (Qwen3.5-9B-OptiQ-4bit, FSM-constrained outlines):
38
- │ fetch session + pm_tasks + recent context
39
- │ → SessionClassification {task_key, session_type, confidence,
40
- │ reasoning, method, dimensions}
41
- │ Rust writes ticket_links + session_dimensions, advances cursor.
34
+ classifies them internally, and writes ticket_links +
35
+ session_dimensions, then advances the cursor.
42
36
 
43
37
  └── pm-worklog (Stage 4) (src/pm_worklog/, Rust → MLX)
44
38
  The Rust hour-driven driver collects each (task, hour) bundle and
@@ -260,7 +254,7 @@ When the screen is locked the selector uses `min(0.8, budget_pct × 1.5)` as the
260
254
  2. **Largest cached model fits** — if the preferred is too large, return the largest catalog model whose files are already in the HF cache and whose `min_ram_gb ≤ budget`. Avoids surprising multi-GB downloads on constrained machines.
261
255
  3. **Largest catalog model that fits (may download)** — if nothing cached fits, return the largest catalog entry where `min_ram_gb ≤ budget`, regardless of cache. This triggers a one-time download of the best available model rather than loading an oversized one that exceeds available memory. Falls back to `preferred_hf_id` only when **no catalog model fits** at all (budget so low even the 1.8 GB model won't load).
262
256
 
263
- **Why stage 3 matters on low-RAM machines:** an M1 Air (8 GB) has Metal headroom ≈ 5.4 GB. At `LLM_BUDGET_PCT=0.5` the budget is ~2.7 GB. The default preferred model is 6.5 GB (`Qwen3.5-9B-OptiQ-4bit`). Without the fix, stage 3 returned the preferred unconditionally — the server then attempted to load a 6.5 GB model into a 2.7 GB budget, causing memory pressure or an outright load failure. With the fix, stage 3 selects `Qwen3.5-4B-MLX-4bit` (2.5 GB) or `Llama-3.2-3B-Instruct-4bit` (1.8 GB) — whichever is largest and fits — and downloads it on first use.
257
+ **Why stage 3 matters on low-RAM machines:** an M1 Air (8 GB) has Metal headroom ≈ 5.4 GB. At `LLM_BUDGET_PCT=0.5` the budget is ~2.7 GB. The default preferred model is 6.5 GB (`Qwen3.5-2B-OptiQ-4bit`). Without the fix, stage 3 returned the preferred unconditionally — the server then attempted to load a 6.5 GB model into a 2.7 GB budget, causing memory pressure or an outright load failure. With the fix, stage 3 selects `Qwen3.5-4B-MLX-4bit` (2.5 GB) or `Llama-3.2-3B-Instruct-4bit` (1.8 GB) — whichever is largest and fits — and downloads it on first use.
264
258
 
265
259
  **Check what would be selected:**
266
260
 
@@ -270,7 +264,7 @@ cd services
270
264
  from agents.llm_selector import select_mlx_model_id, probe_compute
271
265
  snap = probe_compute()
272
266
  print(f'Headroom: {snap.metal_headroom_gb:.1f} GB thermal: {snap.thermal_level}')
273
- model = select_mlx_model_id('mlx-community/Qwen3.5-9B-OptiQ-4bit', 6.5, 0.5)
267
+ model = select_mlx_model_id('mlx-community/Qwen3.5-2B-OptiQ-4bit', 6.5, 0.5)
274
268
  print(f'Would load: {model}')
275
269
  "
276
270
  ```
@@ -350,14 +344,14 @@ pip install -e ".[local-llm]"
350
344
  FROM ticket_links WHERE session_id = <ID>;
351
345
  ```
352
346
 
353
- 2. **Re-run the classifier against a live session** call the MLX server directly with the session id:
347
+ 2. **Re-run the classifier against a live session** using the daemon CLI:
348
+
354
349
  ```bash
355
- curl -s -X POST http://127.0.0.1:7823/classify_sessions \
356
- -H "Content-Type: application/json" \
357
- -d "{\"session_ids\": [<ID>]}" | jq .
350
+ meridian coding-agent-classify
358
351
  ```
359
352
 
360
353
  Or use the standalone MLX script (reads from stdin, prints JSON to stdout):
354
+
361
355
  ```bash
362
356
  cd services
363
357
  echo '{"session_ids": [<ID>], "meridian_db": "'"$HOME"'/.meridian/meridian.db"}' \
@@ -0,0 +1,34 @@
1
+ """Process-global server state shared between the FastAPI app and route modules."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import threading
6
+ from typing import Any
7
+
8
+ # Populated in _lifespan (mlx_module, loaded_at, model_sem, tracer, self_url, …)
9
+ # and read by every route module.
10
+ app_state: dict[str, Any] = {}
11
+
12
+ # Shared prefetch progress, guarded by prefetch_lock. states: idle|downloading|done|error
13
+ prefetch_state: dict[str, Any] = {
14
+ "state": "idle",
15
+ "model_id": None,
16
+ "received": 0,
17
+ "total": 0,
18
+ "error": None,
19
+ }
20
+ prefetch_lock = threading.Lock()
21
+
22
+
23
+ def model_sem() -> "asyncio.Semaphore":
24
+ """Return the process-global single-slot model semaphore.
25
+
26
+ Created once in _lifespan and stored in app_state. Every endpoint that
27
+ runs a model inference acquires this before calling run_in_threadpool so
28
+ that concurrent requests never compete on the GPU.
29
+ """
30
+ sem = app_state.get("model_sem")
31
+ if sem is None: # fallback if called before lifespan (e.g. tests)
32
+ sem = asyncio.Semaphore(1)
33
+ app_state["model_sem"] = sem
34
+ return sem
@@ -0,0 +1,60 @@
1
+ """Minimal AgentOS viewer for browsing worklog pipeline traces.
2
+
3
+ Run this script to expose the agno_traces.db as an AgentOS runtime, then:
4
+ 1. Open https://os.agno.com, sign in (free account)
5
+ 2. Add new OS → Environment: Local → URL: http://localhost:8000
6
+ 3. Click "CONNECT" → refresh the page
7
+
8
+ Alternatively, browse directly at http://localhost:8000 without the hosted UI.
9
+
10
+ The DB path defaults to ~/.meridian/agno_traces.db (set AGNO_TRACE_DB to override).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import os
16
+ from pathlib import Path
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+ from agno.agent import Agent
21
+ from agno.db.sqlite import SqliteDb
22
+ from agno.os import AgentOS
23
+ from agno.workflow import Workflow
24
+
25
+ _DEFAULT_DB = Path("~/.meridian/agno_traces.db").expanduser()
26
+ _db_file = Path(os.environ.get("AGNO_TRACE_DB", "") or _DEFAULT_DB).expanduser()
27
+
28
+ db = SqliteDb(db_file=str(_db_file))
29
+
30
+ _agent = Agent(
31
+ name="Meridian Worklog Pipeline",
32
+ description="Worklog pipeline agent — match sessions to PM tasks and draft worklogs.",
33
+ db=db,
34
+ )
35
+
36
+ # Stub workflow registered so os.agno.com marks the OS as active.
37
+ # The real pipeline runs inside server.py; this is traces-only.
38
+ _workflow = Workflow(
39
+ name="worklog_hour",
40
+ description="Hour-level worklog pipeline: distil → report → rerank → match → draft/propose.",
41
+ db=db,
42
+ )
43
+
44
+ agent_os = AgentOS(
45
+ id="meridian-worklog",
46
+ name="Meridian Worklog Traces",
47
+ description="Viewer for worklog pipeline traces — worklog.hour, match, propose, draft spans.",
48
+ agents=[_agent],
49
+ workflows=[_workflow],
50
+ db=db,
51
+ tracing=True,
52
+ )
53
+
54
+ app = agent_os.get_app()
55
+
56
+ if __name__ == "__main__":
57
+ log.info("trace DB: %s", _db_file)
58
+ log.info("dashboard: http://localhost:8000")
59
+ log.info("control: https://os.agno.com → Add new OS → http://localhost:8000")
60
+ agent_os.serve(app="agno_viewer:app", reload=False, port=8000)