Menu

#321 feat(tray): bundle the non-capture backend into the .app (Gap-2 Bucket 1, slice 1)

closed
nobody
None
2026-06-24
2026-06-20
Anonymous
No

Originally created by: Akarsh-Hegde

Summary

First slice of Gap-2 Bucket 1 (self-contained DMG, the $0/unblocked part). Tauri bundle.resources now copies the daemon + a11y-helper binaries and their launchd plists into Meridian.app/Contents/Resources/backend/, so a first-run install can register them from inside the .app — eliminating npm install -g screenpipe / brew install ffmpeg for these pieces.

Verified

  • Built .app contains Contents/Resources/backend/: meridian (15M daemon), meridian-a11y-helper, com.meridiona.daemon.plist, com.meridiona.a11y-helper.plist.
  • DMG builds clean → Meridian_0.1.0_aarch64.dmg 16 MB (was 10 MB; the +6 is the bundled backend).

Notes

  • Build ordering: the daemon binary is a cargo build --release artifact, which must run before tauri build. The release .releaserc prepareCmd already does this; build it by hand for a local bundle.
  • screenpipe/capture is intentionally NOT bundled — it's relicense-blocked (Gap-2 Bucket 2, in-process capture; spike runbook in Obsidian).

Next slice (1b — not in this PR)

First-run install orchestration: on first launch, copy the bundled binaries → ~/.meridian/bin and register the launchd agents (port install-from-bundle.sh; SMAppService for the non-capture services). This PR is just the bundling foundation.

Scope / context

  • Bucket 1 alone does not yield a "working app" — capture (Bucket 2) is required for that. This is the unblocked foundation + install harness everything reuses.
  • Full Gap-2 scope: Obsidian Decisions/Gap-2 scope - bundle the backend into the .app.

Stacked on feat/mlx-runtime-integrity (#320) → … → spike → main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01M7jfB95RakvdwJ8hRogrs4

Related

Tickets: #322
Tickets: #323
Tickets: #330
Tickets: #347
Tickets: #351

Discussion

  • Anonymous

    Anonymous - 2026-06-20

    Originally posted by: Akarsh-Hegde

    Added the parallel agent's oauth bugfix to this PR.

    f6b8781 — fix(oauth): prevent GITHUB_TOKEN from merging onto prior .env line (cherry-picked from local-staging c88919d, authored by a parallel agent).

    upsert_env_key had two bugs in src/intelligence/oauth/github.rs: the replace path dropped the trailing newline that .lines() strips, and the append path wrote the new key immediately after the last byte without a separator — so keys could merge onto the previous .env line when the file had no trailing newline. Fix preserves the trailing newline on replace and prepends \n before append when needed.

    It had been sitting unpushed on local staging; rather than push it up staging (which is the test-only channel under Option B), it's folded into this PR so it rides the normal feature → mlx-runtime chain → … → main review path. Orthogonal to the bundling change (touches only oauth/github.rs), cherry-picked clean, full pre-push suite green.

     
  • Anonymous

    Anonymous - 2026-06-20

    Originally posted by: Akarsh-Hegde

    Slice 1b — first-run install of the bundled backend (56f7468)

    Builds on slice 1a (which bundled the backend into Resources/backend/). This is the tray side that stages + registers it on the .app DMG path, so the DMG needs no shell installer.

    New module backend_install.rsensure_backend_installed(), spawned off the Tauri setup hook:

    • No-op unless running from a packaged .app (no Resources/backend under tauri dev/source → those keep the shell scripts) and the bundled daemon's SHA-256 differs from the last install (first run / post-update).
    • Stages meridian + meridian-a11y-helper → stable ~/.meridian/bin/ (cmp-guarded copy + chmod 0755), renders the two plists, and runs the launchctl bootout → bootstrap → enable → kickstart dance — a faithful port of install-from-bundle.sh, not SMAppService (the a11y grant is code-hash-keyed and must run from a stable path outside the re-signed .app; SMAppService's Login-Items payoff waits for Dev-ID signing, Bucket 3).
    • Robustness: version marker persisted only after both agents bootstrap (partial failure retries next launch); all failures logged, never crash the tray.

    Env unification: DMG daemon WorkingDirectory = ~/.meridiandotenvy self-loads the canonical ~/.meridian/.env the tray already writes (OTLP left empty in the plist so the daemon self-loads it — a baked value would go stale).

    Ripple (same commit): install.rs meridian_bin() now prefers ~/.meridian/bin/meridian; CLAUDE.md daemon-config gotcha corrected (the old ~/.meridian/app/.env claim was stale — that file is no longer written) + DMG case documented.

    Tests: apply_subs substitution + a guard that the real committed plist templates fully substitute (XML comments stripped) so a new body placeholder can't ship a broken plist. cargo test + full pre-push suite green.

    Verification note: live launchd registration is intentionally not run on a dev machine (would clobber an existing a11y plist / bootstrap a duplicate daemon against meridian.db). It's deferred to the staging DMG E2E pass on a clean account: launch → launchctl print gui/$(id -u)/com.meridiona.daemon loaded → ~/.meridian/logs/daemon.log shows the poll loop → a11y-helper.log shows trust state → second launch is a no-op.

     
  • Anonymous

    Anonymous - 2026-06-20

    Originally posted by: Akarsh-Hegde

    Teardown slice — meridian uninstall (036c59a)

    The inverse of slice 1b's install. Dragging Meridian.app to the Trash leaves the launchd agents 1b registers running forever (no app code runs once the bundle is gone). This subcommand lives in the daemon binary (staged at ~/.meridian/bin/meridian), so it survives the .app being trashed and can clean up its own orphans — which a tray menu item couldn't.

    Turns out SETUP.md already documented meridian uninstall ("stop services and remove the CLI") but the native binary never implemented it — so this fills a promised-but-missing command.

    meridian uninstall [--purge] [--dry-run] [--yes]:

    • Boots out + removes every ~/Library/LaunchAgents/com.meridiona.*.plist (glob → nothing orphaned, any installer).
    • Removes the staged binaries + install marker + the ~/.local/bin CLI entries (DMG symlink or npm node-wrapper).
    • Keeps user data by default (~/.meridian/.env, meridian.db, oauth/, settings.json, runtime/); --purge removes all of ~/.meridian.
    • Prints a plan; confirms on a TTY (refuses non-interactively without --yes); --dry-run changes nothing.

    New src/uninstall.rs (registered in lib.rs, dispatched in main.rs by doctor). Small intentional label/path duplication with backend_install.rs — the daemon crate can't depend on the tray.

    Tests: label matching, agent enumeration from a temp dir, missing-dir = empty. Verified live with --dry-run (correctly enumerated all 3 com.meridiona.* agents + the staged a11y binary + marker + ~/.local/bin entries, preserved user data). Never ran the real delete on the dev machine.

    Follow-ups (not in this slice)

    • meridian doctor check that flags orphaned agents when the .app is gone (the advisor's "+ doctor check").
    • Tray menu item "Uninstall Meridian…" that shells out to this command.
     
  • Anonymous

    Anonymous - 2026-06-24

    Ticket changed by: Akarsh-Hegde

    • status: open --> closed
     

Log in to post a comment.