···4242 pull_request:
4343 push:
4444 tags:
4545- - '**[0-9]+.[0-9]+.[0-9]+*'
4545+ - "**[0-9]+.[0-9]+.[0-9]+*"
46464747jobs:
4848 # Run 'dist plan' (or host) to determine what tasks we need to do
···218218 name: artifacts-build-plugins
219219 path: ${{ env.ARCHIVE }}
220220221221- # Build and package ein-server for each platform so bootstrap.rs can download it
221221+ # Build and package eind for each platform so bootstrap.rs can download it
222222 build-server-artifacts:
223223 name: build-server-artifacts (${{ matrix.target }})
224224 needs:
···249249 uses: arduino/setup-protoc@v3
250250 with:
251251 repo-token: ${{ secrets.GITHUB_TOKEN }}
252252- - name: Build ein-server
253253- run: cargo build --release -p ein-server --target ${{ matrix.target }}
252252+ - name: Build eind
253253+ run: cargo build --release -p eind --target ${{ matrix.target }}
254254 - name: Package archive
255255 shell: bash
256256 run: |
257257 TARGET="${{ matrix.target }}"
258258- ARCHIVE="ein-server-${TARGET}.tar.xz"
258258+ ARCHIVE="eind-${TARGET}.tar.xz"
259259 mkdir -p dist-server
260260- cp "target/${TARGET}/release/ein-server" dist-server/
261261- tar -cJf "${ARCHIVE}" -C dist-server ein-server
260260+ cp "target/${TARGET}/release/eind" dist-server/
261261+ tar -cJf "${ARCHIVE}" -C dist-server eind
262262 echo "ARCHIVE=${ARCHIVE}" >> "$GITHUB_ENV"
263263 - name: Upload server archive artifact
264264 uses: actions/upload-artifact@v6
+5-5
CLAUDE.md
···1212rustup target add wasm32-wasip2
1313cargo build # Build all crates
1414cargo build -p ein-tui # Build just the TUI client
1515-cargo build -p ein-server # Build just the server
1515+cargo build -p eind # Build just the server
1616```
17171818Plugins (tool plugins and model client plugins) are WASM components compiled separately:
···42424343```bash
4444# Terminal 1 — start the server (no env vars needed)
4545-cargo run --bin ein-server
4545+cargo run --bin eind
46464747# Terminal 2 — start the TUI (connects to localhost:50051 by default)
4848cargo run --bin ein-tui
···59596060```
6161┌─────────────────────────────┐ ┌──────────────────────────────┐
6262-│ ein-tui │ gRPC │ ein-server │
6262+│ ein-tui │ gRPC │ eind │
6363│ │ (proto) │ │
6464│ Ratatui terminal UI │◄────────►│ Agent loop + tool executor │
6565│ Keyboard / render loop │ │ WASM plugin host │
···103103- **Save** — after each agent turn, the full message history is serialised and written (`save_messages`).
104104- **Resume** — when a client supplies a known `session_id`, `load_messages` restores the conversation so the agent picks up where it left off.
105105106106-Database migrations live in `crates/ein-server/migrations/`.
106106+Database migrations live in `eind/migrations/`.
107107108108### Client config (`crates/ein-tui/src/config.rs`)
109109···113113114114Legacy flat config files (with top-level `api_key`, `base_url`, `model`, `max_tokens`) are automatically migrated to the nested format on load.
115115116116-### Server (`crates/ein-server/`)
116116+### Server (`eind/`)
117117118118| File | Role |
119119|------|------|
···4455```
66┌─────────────────────────────┐ ┌──────────────────────────────┐
77-│ ein-tui │ gRPC │ ein-server │
77+│ ein-tui │ gRPC │ eind │
88│ │ (proto) │ │
99│ Ratatui terminal UI │◄────────►│ Agent loop + tool executor │
1010│ Session picker on startup │ │ WASM plugin host │
···2323cargo binstall --git https://github.com/mstallmo/ein ein
2424```
25252626-This installs both `ein-tui` (terminal UI) and `ein-server` (gRPC agent server). You can also install them individually:
2626+This installs both `ein-tui` (terminal UI) and `eind` (gRPC agent server). You can also install them individually:
27272828```bash
2929cargo binstall --git https://github.com/mstallmo/ein ein-tui
3030-cargo binstall --git https://github.com/mstallmo/ein ein-server
3030+cargo binstall --git https://github.com/mstallmo/ein eind
3131```
32323333Or download archives directly from [GitHub Releases](https://github.com/mstallmo/ein/releases).
···128128Start the server in one terminal:
129129130130```bash
131131-cargo run --bin ein-server
131131+cargo run --bin eind
132132```
133133134134Start the TUI client in another:
···270270```
271271crates/
272272 ein-proto/ Protocol Buffer definitions (gRPC service + message types)
273273- ein-server/ gRPC server — agent loop, WASM plugin host, session persistence
273273+ eind/ gRPC server — agent loop, WASM plugin host, session persistence
274274 ein-tui/ Terminal UI client
275275packages/
276276 ein_tool/ WASM tool plugin interface (ToolPlugin trait, ToolDef, syscalls)
···315315316316Uses **Ratatui** (v0.29) for rendering and **crossterm** for keyboard events. The conversation pane renders a corgi pixel-art header on startup. Edit diffs are syntax-highlighted using `syntect` with the `base16-ocean.dark` theme.
317317318318-### Server modules (`crates/ein-server/src/`)
318318+### Server modules (`eind/src/`)
319319320320| File | Role |
321321|------|------|
···329329330330## Releasing
331331332332-Releases are fully automated via CI using [cargo-dist](https://axodotdev.github.io/cargo-dist/). Only the `crates/ein` meta-package is distributed — it includes both the `ein-tui` and `ein-server` binaries.
332332+Releases are fully automated via CI using [cargo-dist](https://axodotdev.github.io/cargo-dist/). Only the `crates/ein` meta-package is distributed — it includes both the `ein-tui` and `eind` binaries.
333333334334**1. Bump the version**
335335
···3344//! Ein server library.
55//!
66-//! Exposes [`run`] so both the standalone `ein-server` binary and the `ein`
66+//! Exposes [`run`] so both the standalone `eind` binary and the `ein`
77//! meta-package binary can share the same entry-point without duplicating code.
8899mod grpc;
···72727373 let server = AgentServer::new().await?;
74747575- println!("ein-server listening on {addr}");
7575+ println!("eind listening on {addr}");
76767777 Server::builder()
7878 .add_service(AgentServiceServer::new(server))
+2-2
crates/ein-server/src/main.rs
eind/src/main.rs
···2929 let args = Args::parse();
30303131 match args.command {
3232- Some(Commands::InstallPlugins { version }) => ein_server::install_plugins(version).await,
3333- None => ein_server::run(args.port).await,
3232+ Some(Commands::InstallPlugins { version }) => eind::install_plugins(version).await,
3333+ None => eind::run(args.port).await,
3434 }
3535}
···242242243243 // Mount each allowed path at its absolute guest path so plugins can
244244 // open files by absolute path. Additionally mount the first path as
245245- // "." so that relative paths (e.g. "crates/ein-server/Cargo.toml")
245245+ // "." so that relative paths (e.g. "crates/eind/Cargo.toml")
246246 // resolve correctly — WASI resolves relative paths against the guest
247247 // current directory, which must be explicitly preopened.
248248 let mut first = true;
···11// SPDX-License-Identifier: Apache-2.0
22// Copyright 2026 Mason Stallmo
3344-//! Bootstrap logic: downloads `ein-server` on first run and registers it as a
44+//! Bootstrap logic: downloads `eind` on first run and registers it as a
55//! system service (macOS LaunchAgent or Linux systemd user service).
6677// These items are only called from the #[cfg(not(debug_assertions))] block in
···1414 os::unix::fs::PermissionsExt,
1515 path::{Path, PathBuf},
1616};
1717-use xz2::read::XzDecoder;
1817use tar::Archive;
1918use tokio::{fs, process::Command, task};
1919+use xz2::read::XzDecoder;
20202121const GITHUB_REPO: &str = "mstallmo/ein";
22222323-/// Path where `ein` installs the server binary: `~/.ein/bin/ein-server`.
2323+/// Path where `ein` installs the server binary: `~/.ein/bin/eind`.
2424pub fn server_bin_path() -> PathBuf {
2525 dirs::home_dir()
2626 .expect("home directory not found")
2727 .join(".ein")
2828 .join("bin")
2929- .join("ein-server")
2929+ .join("eind")
3030}
31313232/// Compile-time target triple used to select the right GitHub release asset.
···4343 ""
4444}
45454646-/// Downloads the `ein-server` binary for the current platform from GitHub
4747-/// releases and writes it to `~/.ein/bin/ein-server` with executable permissions.
4646+/// Downloads the `eind` binary for the current platform from GitHub
4747+/// releases and writes it to `~/.ein/bin/eind` with executable permissions.
4848pub async fn download_server(version: &str) -> Result<()> {
4949 let ver = version.trim_start_matches('v');
5050 let tag = format!("v{ver}");
5151 let triple = target_triple();
5252 // cargo-dist names archives as "{package}-{triple}.tar.xz" (no version in filename).
5353- let archive_name = format!("ein-server-{triple}.tar.xz");
5353+ let archive_name = format!("eind-{triple}.tar.xz");
5454 let url = format!("https://github.com/{GITHUB_REPO}/releases/download/{tag}/{archive_name}");
55555656 let dest = server_bin_path();
···8383 perms.set_mode(0o755);
8484 fs::set_permissions(&dest, perms).await?;
85858686- println!("ein-server installed to {}", dest.display());
8686+ println!("eind installed to {}", dest.display());
8787 Ok(())
8888}
89899090-/// Extracts the `ein-server` binary from a tar.xz archive into `dest`.
9090+/// Extracts the `eind` binary from a tar.xz archive into `dest`.
9191fn extract_server(bytes: &[u8], dest: &Path) -> Result<()> {
9292 let xz = XzDecoder::new(io::Cursor::new(bytes));
9393 let mut archive = Archive::new(xz);
···9999 let mut entry = entry.context("corrupt archive entry")?;
100100 let entry_path = entry.path().context("entry has no path")?;
101101102102- // The archive contains exactly one file: the `ein-server` binary.
102102+ // The archive contains exactly one file: the `eind` binary.
103103 // Accept it regardless of any leading directory component.
104104 let file_name = entry_path
105105 .file_name()
106106 .and_then(|n| n.to_str())
107107 .unwrap_or("");
108108109109- if file_name == "ein-server" {
109109+ if file_name == "eind" {
110110 let mut file = std::fs::File::create(dest)
111111 .with_context(|| format!("failed to create {}", dest.display()))?;
112112- io::copy(&mut entry, &mut file).context("failed to write ein-server")?;
112112+ io::copy(&mut entry, &mut file).context("failed to write eind")?;
113113 return Ok(());
114114 }
115115 }
116116117117- anyhow::bail!("ein-server binary not found in archive")
117117+ anyhow::bail!("eind binary not found in archive")
118118}
119119120120// ---------------------------------------------------------------------------
121121// Service registration
122122// ---------------------------------------------------------------------------
123123124124-/// Ensures `ein-server` is registered as a system service.
124124+/// Ensures `eind` is registered as a system service.
125125///
126126/// On macOS, installs a LaunchAgent plist and loads it.
127127/// On Linux, writes a systemd user unit and enables it.
···141141// Uninstall
142142// ---------------------------------------------------------------------------
143143144144-/// Stops and removes the `ein-server` service and binary installed by
144144+/// Stops and removes the `eind` service and binary installed by
145145/// [`ensure_service_installed`] and [`download_server`].
146146///
147147/// Returns a list of completed step descriptions for display in the TUI.
···292292 anyhow::bail!("launchctl load failed: {stderr}");
293293 }
294294295295- println!("ein-server registered as LaunchAgent ({LAUNCH_AGENT_LABEL})");
295295+ println!("eind registered as LaunchAgent ({LAUNCH_AGENT_LABEL})");
296296 Ok(())
297297}
298298···301301// ---------------------------------------------------------------------------
302302303303#[cfg(target_os = "linux")]
304304-const SYSTEMD_SERVICE_NAME: &str = "ein-server";
304304+const SYSTEMD_SERVICE_NAME: &str = "eind";
305305306306#[cfg(target_os = "linux")]
307307fn systemd_unit_path() -> PathBuf {
···363363 anyhow::bail!("systemctl enable failed: {stderr}");
364364 }
365365366366- println!("ein-server registered as systemd user service ({SYSTEMD_SERVICE_NAME})");
366366+ println!("eind registered as systemd user service ({SYSTEMD_SERVICE_NAME})");
367367 Ok(())
368368}
+3-3
crates/ein-tui/src/input.rs
···66use tracing::{debug, info, warn};
7788use crate::app::{
99- App, CwdState, DisplayMessage, Modal, SessionPickerState, SetupWizardState, UninstallModalState,
1010- UninstallPhase, WizardStep,
99+ App, CwdState, DisplayMessage, Modal, SessionPickerState, SetupWizardState,
1010+ UninstallModalState, UninstallPhase, WizardStep,
1111};
1212use crate::connection::to_proto_session_config;
1313···5959 },
6060 CommandDef {
6161 name: "/uninstall",
6262- description: "Stop and remove the ein-server service and binary",
6262+ description: "Stop and remove the eind service and binary",
6363 },
6464];
6565
+2-2
crates/ein-tui/src/lib.rs
···98989999 info!(server_addr = %args.server_addr, "ein-tui starting");
100100101101- // In release builds: download ein-server if absent, then register it as a
101101+ // In release builds: download eind if absent, then register it as a
102102 // system service. Runs before raw mode so stdout is visible for progress.
103103 #[cfg(not(debug_assertions))]
104104 {
105105 let bin = bootstrap::server_bin_path();
106106 if !bin.exists() {
107107- println!("Downloading ein-server {}...", env!("CARGO_PKG_VERSION"));
107107+ println!("Downloading eind {}...", env!("CARGO_PKG_VERSION"));
108108 bootstrap::download_server(env!("CARGO_PKG_VERSION")).await?;
109109 }
110110 bootstrap::ensure_service_installed().await?;
···11+// SPDX-License-Identifier: Apache-2.0
22+// Copyright 2026 Mason Stallmo
33+44+//! Bootstrap logic: downloads `eind` on first run and registers it as a
55+//! system service (macOS LaunchAgent or Linux systemd user service).
66+77+// These items are only called from the #[cfg(not(debug_assertions))] block in
88+// lib.rs, so they appear unused in debug builds. That's intentional.
99+#![cfg_attr(debug_assertions, allow(dead_code))]
1010+1111+use anyhow::{Context, Result};
1212+use std::{
1313+ io,
1414+ os::unix::fs::PermissionsExt,
1515+ path::{Path, PathBuf},
1616+};
1717+use tar::Archive;
1818+use tokio::{fs, process::Command, task};
1919+use xz2::read::XzDecoder;
2020+2121+const GITHUB_REPO: &str = "mstallmo/ein";
2222+2323+/// Path where `ein` installs the server binary: `~/.ein/bin/eind`.
2424+pub fn server_bin_path() -> PathBuf {
2525+ dirs::home_dir()
2626+ .expect("home directory not found")
2727+ .join(".ein")
2828+ .join("bin")
2929+ .join("eind")
3030+}
3131+3232+/// Compile-time target triple used to select the right GitHub release asset.
3333+pub fn target_triple() -> &'static str {
3434+ #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
3535+ return "aarch64-apple-darwin";
3636+ #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
3737+ return "x86_64-apple-darwin";
3838+ #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
3939+ return "aarch64-unknown-linux-gnu";
4040+ #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4141+ return "x86_64-unknown-linux-gnu";
4242+ #[allow(unreachable_code)]
4343+ ""
4444+}
4545+4646+/// Downloads the `eind` binary for the current platform from GitHub
4747+/// releases and writes it to `~/.ein/bin/eind` with executable permissions.
4848+pub async fn download_server(version: &str) -> Result<()> {
4949+ let ver = version.trim_start_matches('v');
5050+ let tag = format!("v{ver}");
5151+ let triple = target_triple();
5252+ // cargo-dist names archives as "{package}-{triple}.tar.xz" (no version in filename).
5353+ let archive_name = format!("eind-{triple}.tar.xz");
5454+ let url = format!("https://github.com/{GITHUB_REPO}/releases/download/{tag}/{archive_name}");
5555+5656+ let dest = server_bin_path();
5757+ fs::create_dir_all(dest.parent().unwrap())
5858+ .await
5959+ .context("failed to create ~/.ein/bin")?;
6060+6161+ println!("Downloading {url}...");
6262+6363+ let response = reqwest::get(&url)
6464+ .await
6565+ .with_context(|| format!("failed to fetch {url}"))?;
6666+6767+ if !response.status().is_success() {
6868+ anyhow::bail!("download failed: HTTP {}", response.status());
6969+ }
7070+7171+ let bytes = response
7272+ .bytes()
7373+ .await
7474+ .context("failed to read response body")?;
7575+7676+ let dest_clone = dest.clone();
7777+ task::spawn_blocking(move || extract_server(&bytes, &dest_clone))
7878+ .await
7979+ .context("extraction task panicked")??;
8080+8181+ // Make the binary executable.
8282+ let mut perms = fs::metadata(&dest).await?.permissions();
8383+ perms.set_mode(0o755);
8484+ fs::set_permissions(&dest, perms).await?;
8585+8686+ println!("eind installed to {}", dest.display());
8787+ Ok(())
8888+}
8989+9090+/// Extracts the `eind` binary from a tar.xz archive into `dest`.
9191+fn extract_server(bytes: &[u8], dest: &Path) -> Result<()> {
9292+ let xz = XzDecoder::new(io::Cursor::new(bytes));
9393+ let mut archive = Archive::new(xz);
9494+9595+ for entry in archive
9696+ .entries()
9797+ .context("failed to read archive entries")?
9898+ {
9999+ let mut entry = entry.context("corrupt archive entry")?;
100100+ let entry_path = entry.path().context("entry has no path")?;
101101+102102+ // The archive contains exactly one file: the `eind` binary.
103103+ // Accept it regardless of any leading directory component.
104104+ let file_name = entry_path
105105+ .file_name()
106106+ .and_then(|n| n.to_str())
107107+ .unwrap_or("");
108108+109109+ if file_name == "eind" {
110110+ let mut file = std::fs::File::create(dest)
111111+ .with_context(|| format!("failed to create {}", dest.display()))?;
112112+ io::copy(&mut entry, &mut file).context("failed to write eind")?;
113113+ return Ok(());
114114+ }
115115+ }
116116+117117+ anyhow::bail!("eind binary not found in archive")
118118+}
119119+120120+// ---------------------------------------------------------------------------
121121+// Service registration
122122+// ---------------------------------------------------------------------------
123123+124124+/// Ensures `eind` is registered as a system service.
125125+///
126126+/// On macOS, installs a LaunchAgent plist and loads it.
127127+/// On Linux, writes a systemd user unit and enables it.
128128+/// On other platforms, does nothing (the TUI's retry loop handles reconnects).
129129+pub async fn ensure_service_installed() -> Result<()> {
130130+ #[cfg(target_os = "macos")]
131131+ return ensure_launchagent_installed().await;
132132+133133+ #[cfg(target_os = "linux")]
134134+ return ensure_systemd_installed().await;
135135+136136+ #[cfg(not(any(target_os = "macos", target_os = "linux")))]
137137+ Ok(())
138138+}
139139+140140+// ---------------------------------------------------------------------------
141141+// Uninstall
142142+// ---------------------------------------------------------------------------
143143+144144+/// Stops and removes the `eind` service and binary installed by
145145+/// [`ensure_service_installed`] and [`download_server`].
146146+///
147147+/// Returns a list of completed step descriptions for display in the TUI.
148148+/// User config and session data in `~/.ein/` are left intact.
149149+pub async fn uninstall() -> Result<Vec<String>> {
150150+ let mut steps: Vec<String> = Vec::new();
151151+ #[cfg(target_os = "macos")]
152152+ uninstall_launchagent(&mut steps).await?;
153153+ #[cfg(target_os = "linux")]
154154+ uninstall_systemd(&mut steps).await?;
155155+ remove_server_binary(&mut steps).await?;
156156+ Ok(steps)
157157+}
158158+159159+async fn remove_server_binary(steps: &mut Vec<String>) -> Result<()> {
160160+ let path = server_bin_path();
161161+ if path.exists() {
162162+ fs::remove_file(&path)
163163+ .await
164164+ .with_context(|| format!("failed to remove {}", path.display()))?;
165165+ steps.push(format!("Removed {}", path.display()));
166166+ }
167167+ Ok(())
168168+}
169169+170170+#[cfg(target_os = "macos")]
171171+async fn uninstall_launchagent(steps: &mut Vec<String>) -> Result<()> {
172172+ let plist = launchagent_plist_path();
173173+ // Ignore errors — the service may already be stopped/unloaded.
174174+ let _ = Command::new("launchctl")
175175+ .args(["unload", plist.to_str().unwrap_or("")])
176176+ .output()
177177+ .await;
178178+ if plist.exists() {
179179+ fs::remove_file(&plist)
180180+ .await
181181+ .with_context(|| format!("failed to remove {}", plist.display()))?;
182182+ steps.push(format!("Removed LaunchAgent ({})", LAUNCH_AGENT_LABEL));
183183+ }
184184+ Ok(())
185185+}
186186+187187+#[cfg(target_os = "linux")]
188188+async fn uninstall_systemd(steps: &mut Vec<String>) -> Result<()> {
189189+ let unit = systemd_unit_path();
190190+ let _ = Command::new("systemctl")
191191+ .args(["--user", "stop", SYSTEMD_SERVICE_NAME])
192192+ .output()
193193+ .await;
194194+ let _ = Command::new("systemctl")
195195+ .args(["--user", "disable", SYSTEMD_SERVICE_NAME])
196196+ .output()
197197+ .await;
198198+ if unit.exists() {
199199+ fs::remove_file(&unit)
200200+ .await
201201+ .with_context(|| format!("failed to remove {}", unit.display()))?;
202202+ steps.push(format!(
203203+ "Removed systemd user service ({})",
204204+ SYSTEMD_SERVICE_NAME
205205+ ));
206206+ }
207207+ let _ = Command::new("systemctl")
208208+ .args(["--user", "daemon-reload"])
209209+ .output()
210210+ .await;
211211+ Ok(())
212212+}
213213+214214+// ---------------------------------------------------------------------------
215215+// macOS LaunchAgent
216216+// ---------------------------------------------------------------------------
217217+218218+#[cfg(target_os = "macos")]
219219+const LAUNCH_AGENT_LABEL: &str = "com.ein.eind";
220220+221221+#[cfg(target_os = "macos")]
222222+fn launchagent_plist_path() -> PathBuf {
223223+ dirs::home_dir()
224224+ .expect("home directory not found")
225225+ .join("Library")
226226+ .join("LaunchAgents")
227227+ .join(format!("{LAUNCH_AGENT_LABEL}.plist"))
228228+}
229229+230230+#[cfg(target_os = "macos")]
231231+async fn ensure_launchagent_installed() -> Result<()> {
232232+ // Check if already loaded.
233233+ let status = Command::new("launchctl")
234234+ .args(["list", LAUNCH_AGENT_LABEL])
235235+ .output()
236236+ .await
237237+ .context("launchctl not found")?;
238238+239239+ if status.status.success() {
240240+ return Ok(()); // Already running.
241241+ }
242242+243243+ let plist_path = launchagent_plist_path();
244244+ let bin = server_bin_path();
245245+ let log = dirs::home_dir()
246246+ .expect("home directory not found")
247247+ .join(".ein")
248248+ .join("server.log");
249249+250250+ let plist = format!(
251251+ r#"<?xml version="1.0" encoding="UTF-8"?>
252252+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
253253+<plist version="1.0">
254254+<dict>
255255+ <key>Label</key>
256256+ <string>{LAUNCH_AGENT_LABEL}</string>
257257+ <key>ProgramArguments</key>
258258+ <array>
259259+ <string>{bin}</string>
260260+ </array>
261261+ <key>RunAtLoad</key>
262262+ <true/>
263263+ <key>KeepAlive</key>
264264+ <true/>
265265+ <key>StandardOutPath</key>
266266+ <string>{log}</string>
267267+ <key>StandardErrorPath</key>
268268+ <string>{log}</string>
269269+</dict>
270270+</plist>
271271+"#,
272272+ LAUNCH_AGENT_LABEL = LAUNCH_AGENT_LABEL,
273273+ bin = bin.display(),
274274+ log = log.display(),
275275+ );
276276+277277+ fs::create_dir_all(plist_path.parent().unwrap())
278278+ .await
279279+ .context("failed to create LaunchAgents directory")?;
280280+ fs::write(&plist_path, plist)
281281+ .await
282282+ .context("failed to write plist")?;
283283+284284+ let output = Command::new("launchctl")
285285+ .args(["load", plist_path.to_str().unwrap()])
286286+ .output()
287287+ .await
288288+ .context("failed to run launchctl load")?;
289289+290290+ if !output.status.success() {
291291+ let stderr = String::from_utf8_lossy(&output.stderr);
292292+ anyhow::bail!("launchctl load failed: {stderr}");
293293+ }
294294+295295+ println!("eind registered as LaunchAgent ({LAUNCH_AGENT_LABEL})");
296296+ Ok(())
297297+}
298298+299299+// ---------------------------------------------------------------------------
300300+// Linux systemd user service
301301+// ---------------------------------------------------------------------------
302302+303303+#[cfg(target_os = "linux")]
304304+const SYSTEMD_SERVICE_NAME: &str = "eind";
305305+306306+#[cfg(target_os = "linux")]
307307+fn systemd_unit_path() -> PathBuf {
308308+ dirs::home_dir()
309309+ .expect("home directory not found")
310310+ .join(".config")
311311+ .join("systemd")
312312+ .join("user")
313313+ .join(format!("{SYSTEMD_SERVICE_NAME}.service"))
314314+}
315315+316316+#[cfg(target_os = "linux")]
317317+async fn ensure_systemd_installed() -> Result<()> {
318318+ // Check if already enabled.
319319+ let status = Command::new("systemctl")
320320+ .args(["--user", "is-enabled", SYSTEMD_SERVICE_NAME])
321321+ .output()
322322+ .await
323323+ .context("systemctl not found")?;
324324+325325+ if status.status.success() {
326326+ return Ok(()); // Already enabled.
327327+ }
328328+329329+ let unit_path = systemd_unit_path();
330330+ let bin = server_bin_path();
331331+332332+ let unit = format!(
333333+ "[Unit]\nDescription=Ein server\n\n[Service]\nExecStart={bin}\nRestart=always\n\n[Install]\nWantedBy=default.target\n",
334334+ bin = bin.display(),
335335+ );
336336+337337+ fs::create_dir_all(unit_path.parent().unwrap())
338338+ .await
339339+ .context("failed to create systemd user directory")?;
340340+ fs::write(&unit_path, unit)
341341+ .await
342342+ .context("failed to write systemd unit")?;
343343+344344+ let reload = Command::new("systemctl")
345345+ .args(["--user", "daemon-reload"])
346346+ .output()
347347+ .await
348348+ .context("failed to run systemctl daemon-reload")?;
349349+350350+ if !reload.status.success() {
351351+ let stderr = String::from_utf8_lossy(&reload.stderr);
352352+ anyhow::bail!("systemctl daemon-reload failed: {stderr}");
353353+ }
354354+355355+ let enable = Command::new("systemctl")
356356+ .args(["--user", "enable", "--now", SYSTEMD_SERVICE_NAME])
357357+ .output()
358358+ .await
359359+ .context("failed to run systemctl enable")?;
360360+361361+ if !enable.status.success() {
362362+ let stderr = String::from_utf8_lossy(&enable.stderr);
363363+ anyhow::bail!("systemctl enable failed: {stderr}");
364364+ }
365365+366366+ println!("eind registered as systemd user service ({SYSTEMD_SERVICE_NAME})");
367367+ Ok(())
368368+}
+3-3
specs/EIN-SERVER-CODE-REVIEW-v1.md
···11-# ein-server Code Review Report
11+# eind Code Review Report
2233_Evaluation Date: 2026-04-03_
44···6677## Executive Summary
8899-The `ein-server` (located in `crates/ein-server`) is a sophisticated gRPC server that implements a secure WASM-based agent hosting model. It successfully implements the core architecture of:
99+The `eind` (located in `eind`) is a sophisticated gRPC server that implements a secure WASM-based agent hosting model. It successfully implements the core architecture of:
1010- Loading and orchestrating WASM WASI plugins with **per-session isolation**
1111- Fine-grained **access controls** (filesystem paths, network hosts)
1212- Graceful **error handling** (preserving sessions after errors)
···807807808808## Conclusion
809809810810-**The `ein-server` is a well-architected and innovative implementation** of a secure WASM-based agent hosting model. Its design prioritizes:
810810+**The `eind` is a well-architected and innovative implementation** of a secure WASM-based agent hosting model. Its design prioritizes:
811811- ✅ Session isolation
812812- ✅ Fine-grained access control
813813- ✅ Graceful error handling
+3-3
specs/EIN-SERVER-CODE-REVIEW-v2.md
···11-# ein-server Code Review Report
11+# eind Code Review Report
2233_Evaluation Date: 2026-04-04_
44_Rated: v0.1.0_
···7788## Executive Summary
991010-The `ein-server` (located in `crates/ein-server`) is a sophisticated gRPC server that implements a secure WASM-based agent hosting model. It successfully implements the core architecture of:
1010+The `eind` (located in `eind`) is a sophisticated gRPC server that implements a secure WASM-based agent hosting model. It successfully implements the core architecture of:
11111212- Loading and orchestrating WASM WASI plugins with **per-session isolation**
1313- Fine-grained **access controls** (filesystem paths, network hosts)
···461461462462## Conclusion
463463464464-The `ein-server` is well-architected for Wasm plugin execution, with a clear separation of concerns. Security concerns around shell command execution must be addressed first. Error handling and metric instrumentation would significantly improve user experience and debugging. With the fixes above, `ein-server` will be a robust, observable, and secure component of the `ein` ecosystem.
464464+The `eind` is well-architected for Wasm plugin execution, with a clear separation of concerns. Security concerns around shell command execution must be addressed first. Error handling and metric instrumentation would significantly improve user experience and debugging. With the fixes above, `eind` will be a robust, observable, and secure component of the `ein` ecosystem.
465465466466---
467467