Skip to content

DamageLabs/meridian

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

164 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Meridian

A self-hosted, multi-channel agentic assistant in the spirit of OpenClaw. Meridian takes a message from a channel, assembles context, calls an LLM with a tool schema, executes the tools the model asks for, and loops until the task is done.

Built on a small, boring stack: Express + SQLite, routing across hosted providers (Claude, OpenAI, Gemini) and a local Ollama instance.

πŸ“„ Full spec: docs/meridian-prd.md Β· πŸ—οΈ How it's built: docs/system-design.md

What works today

Meridian is usable across three channels, sharing one agent core:

  • CLI β€” one-shot meridian "..." (and piped input); runs on Node built-ins, no install.
  • TUI β€” a live Ink chat shell: streaming markdown, tool activity, inline approvals, input history/multiline, cancel, slash commands.
  • Web UI β€” an auth'd localhost server (meridian serve) with a React/Tailwind chat (streaming, tool activity, approvals, cancel, runtime controls, light/dark) and a dashboard of runs/traces and token/cost charts.

Under the hood:

  • 5 providers behind one router β€” offline stub, local Ollama, and hosted Anthropic, OpenAI, Gemini (native raw-fetch adapters: streaming, tool calls, usage, cancellation).
  • Bounded agent loop β€” multi-step tool execution with a step ceiling, idle timeout, and operator cancellation.
  • Safe tools β€” read-only filesystem tools always on; write_file/run_shell are deny-by-default, approval-gated, and run in an OS sandbox (no network, writes confined to the project root).
  • Safety β€” secret redaction and prompt-injection awareness on untrusted tool output.
  • Observability β€” full per-run traces and token/cost accounting, queryable from the CLI, TUI, and web dashboard.
  • Storage β€” SQLite (WAL) for sessions, messages, and runs.
  • Tested β€” node:test suite + a deterministic eval gate that runs on every change.

See the roadmap for what's next (robustness, Slack, memory).


The thesis

The agent loop is the easy part now. It's a commoditized afternoon of work. The real cost β€” and the real risk β€” lives in channel integrations, the safety guardrails around tool execution, and the maintenance tail of a non-deterministic system.

  • A useful single-channel version is a weekend.
  • Matching OpenClaw's breadth and robustness is months.
  • That gap is almost entirely robustness, not features.

This project is sequenced by dependency and risk, not perceived difficulty β€” because the cheap-looking parts aren't where projects like this fail.

Architecture

flowchart TD
    channels["channels<br/>(CLI, HTTP, Slack, ...)"] --> ingress["ingress<br/>normalize to internal Message"]
    ingress --> loop["agent loop"]
    loop <--> router["model router"]
    router --> providers["Claude / OpenAI / Gemini / Ollama"]
    loop --> tools["tool runtime"]
    tools <--> sandbox["sandbox"]
    loop --> state[("SQLite state<br/>sessions Β· messages Β· summaries<br/>tool logs Β· traces")]
Loading

Workstreams (cheap β†’ expensive)

Area Reality
Agentic loop Easy happy path (~1 day); the robust version β€” cancellation, retries, streaming, mid-loop context management β€” is where the hard things converge.
Model routing Built as native raw-fetch adapters (no SDK/LiteLLM). Chat shapes normalize; tool-call/streaming behavior doesn't β€” each provider needed its own translation.
Tool / skill system Moderate. Registry is easy; sandboxing is the real design cost (don't let a hallucinated rm -rf ruin your week).
Memory / context Moderate. Session history is trivial; durable memory is an open problem β€” accept the ~60–80% ceiling and move on.
Channel integrations The expensive part. Each channel = its own auth, rate limits, formatting, delivery, idempotency. This is the months.

Don't skip these

Load-bearing for a non-deterministic system that touches real resources:

  • Evals β€” highest hidden cost; decide the strategy before the loop.
  • Observability β€” full per-run traces + token/cost accounting.
  • Prompt injection & safety β€” untrusted channel content reaches the tool executor.
  • SQLite write contention β€” WAL + a write queue.

Roadmap

Milestone Scope Status
M0 Eval skeleton β€” representative tasks + runner βœ…
M1 Weekend agent β€” loop + routing + tools + one channel + sessions βœ…
M2 Safe tools β€” sandbox, deny-by-default destructive ops, logging βœ…
M3 Robust loop β€” cancellation, streaming, context management 🚧 cancellation + streaming done; retries (#14) and large-output/context mgmt (#16) remain
M4 Channels β€” prove the adapter abstraction 🚧 CLI + TUI + Web shipped; Slack (#18/#19) remains
M5 Memory + observability β€” tracing, cost, retrieval 🚧 tracing + cost + dashboards done; cross-session memory (#21) remains

Also pending: SQLite write-queue for concurrency (#17). Tracked on the project board and issues.

Development

The core (CLI, agent loop, router, eval harness) runs on Node β‰₯ 22 built-ins alone β€” no install required. The TUI (Ink + React) and web UI (Vite + React + Tailwind + Recharts) pull in dependencies via npm install; both are lazy-loaded so the core CLI stays install-free.

Install the meridian command once (symlinks the local CLI onto your PATH):

npm link

Then set your provider so you don't pass it every time. Config is read from ./.env (project-local) and ~/.config/meridian/.env (user-level), with real environment variables overriding both. Use the user-level file if you run the global meridian command from outside the project directory:

cp .env.example .env                                   # project-local, or:
mkdir -p ~/.config/meridian && cp .env.example ~/.config/meridian/.env
# edit it: MERIDIAN_PROVIDER=ollama, MERIDIAN_MODEL=qwen2.5:7b
# Talk to the assistant (CLI is the first channel β€” #7).
# With no .env, the offline `stub` provider echoes input β€” no API keys needed.
meridian "say hello"
echo "piped request" | meridian

# With .env set to Ollama, it runs a real multi-step loop and can use read-only
# tools (read_file, list_dir) confined to the project root.
meridian "list the files in eval/tasks and tell me how many there are"

# Override config per-run inline (wins over .env):
MERIDIAN_PROVIDER=ollama MERIDIAN_MODEL=qwen2.5:7b meridian "summarize the PRD goals"

# Hosted providers (#4): set the key + select the provider/model (see .env.example).
#   anthropic β†’ ANTHROPIC_API_KEY, MERIDIAN_MODEL=claude-opus-4-8
#   openai    β†’ OPENAI_API_KEY,    MERIDIAN_MODEL=gpt-4o      (OPENAI_BASE_URL for compatible APIs)
#   gemini    β†’ GEMINI_API_KEY,    MERIDIAN_MODEL=gemini-1.5-flash

# Run the eval suite (gates every change, PRD Β§9) and the unit tests.
npm run eval
npm test

# Inspect what the agent actually did (#8): every model call, tool call
# (with inputs/outputs/timing), token usage, and the final reply.
meridian trace          # list recent runs
meridian trace <id>     # show one run's full trace (id prefix is enough)

# Token & cost totals per session (#20). Local models are free; hosted costs
# are estimates from a pricing snapshot.
meridian cost

# Or just ask the agent β€” it has a read-only token_usage tool:
meridian "how many tokens have I used?"

# Interactive TUI (#28) β€” a live chat shell. Unlike the one-shot CLI above,
# this needs dependencies (Ink + React), so run `npm install` first:
npm install && meridian tui      # or: npm run tui

The TUI is lazy-loaded β€” meridian "...", trace, and cost still run on Node built-ins with no install. Only meridian tui pulls in Ink.

Inside the TUI: replies stream live and render as markdown (#37), tool calls show as they run, and a status bar tracks model/tokens/cost. Keys: Up/Down recall input history, \ + Enter adds a newline (Enter submits), Esc cancels an in-flight run, Ctrl-C quits. Guarded tools (write_file/run_shell) prompt for approval ([y]es / [n]o / [a]lways).

Slash commands (#34): /help, /model [name] and /provider [name] (switch live), /tools, /tokens (session usage + cost), /new (fresh session), /clear, /quit.

Web UI (epic #66)

npm run web:build           # build the Vite/React/Tailwind frontend β†’ web/dist
meridian serve              # http://127.0.0.1:8787, prints a bearer token

Open the printed URL for the web UI:

  • Chat (#74) β€” a streaming conversation with markdown replies; runs are recorded so they show up in the dashboard. Shows live tool activity as calls run (#75), a stop button to cancel an in-flight run (#77), and a controls bar to switch model / start a new session live (#78). Guarded tools (write_file/run_shell) prompt for allow/deny in the browser before running (#76), then execute sandboxed.
  • Dashboard (#71/#72) β€” runs list + click-through trace view, session totals, and interactive Recharts charts (#86): cost-over-time, tokens-per-day, and tokens-by-model, over the data in meridian.db. (Recharts is lazy-loaded, so the chat view stays light.)

The header has a light/dark theme toggle (#87) β€” it defaults to your OS preference and remembers your choice.

For frontend dev with hot reload: npm run web:dev (proxies the API to a running meridian serve).

Bound to localhost with bearer-token auth β€” every /api/* request needs the token (/health is open; the served page injects the token for the SPA). ⚠️ Binding to a non-localhost host exposes an agent that can run shell/write files; keep the token secret.

Prefer not to npm link? Everything also works via node --disable-warning=ExperimentalWarning src/cli/index.js "..." (the flag silences the one-off node:sqlite experimental notice; the meridian bin and npm start already include it).

Guarded tools (write_file, run_shell) are denied by default (#9) β€” not even shown to the model unless you opt in. Two ways to allow them:

  • Statically (works everywhere, incl. the one-shot CLI): MERIDIAN_ALLOW_TOOLS=write_file,run_shell or MERIDIAN_ALLOW_WRITES=1.
  • Interactively in the TUI (#32): guarded tools are offered to the model, and when it requests one the TUI prompts you [y]es / [n]o / [a]lways before it runs β€” no env flags needed.

When allowed, run_shell runs inside an OS sandbox (#10): no network, and writes confined to the project root (macOS sandbox-exec; it refuses to run unsandboxed on platforms without support yet). Read tools are always available.

Secrets are redacted from tool output before it reaches the model or the session history (#12): the literal values of secret-looking env vars plus common token shapes (OpenAI/GitHub/AWS/etc.) become [REDACTED]. Best-effort defense in depth, not a guarantee. See .env.example.

Layout

Path Role
src/cli/ Β· src/ingress/ First channel: CLI entry + ingress (#7)
src/agent/ Agent loop β€” bounded multi-step tool execution (#3)
src/router/ Model router β€” stub, Ollama, Anthropic, OpenAI, and Gemini providers (#4)
src/tools/ Tool registry + read-only filesystem tools (#5)
src/store/ SQLite session state (#6)
src/core/ Shared Message/Reply types + config
eval/ Eval harness + task suite (#2)

Status

Working across three channels (CLI, TUI, Web) with five model providers, sandboxed tools, approvals, full tracing, and cost accounting. The remaining work is robustness and reach β€” retries, context management, a SQLite write-queue, Slack, and cross-session memory (see the roadmap). Tracked on the project board and issues.

License

Licensed under the Apache License 2.0 β€” see LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages