Configuration
Config Directory
Melian's configuration lives in ~/.melian/, split across multiple files, one per subsystem. Each file is independently loaded, so you only need to create configs for the features you use. Missing files fall back to sensible defaults (usually "disabled").
~/.melian/
├── config.yaml # Server, LLM, Qdrant, embeddings
├── email.yaml # Email accounts & permissions
├── imessage.yaml # iMessage contacts & modes
├── calendar.yaml # Google Calendar settings
├── tasks.yaml # Obsidian vault & Remarkable sync
├── govee.yaml # Govee smart home
├── wiz.yaml # Wiz smart lights
├── mcp.json # MCP server definitions
├── personality.yaml # Personality traits & context overrides
├── memory/
│ └── core.json # Core memory (auto-managed)
└── spirit.md # Spirit entries (auto-managed)Runtime data (databases) lives separately:
- macOS:
~/Library/Application Support/melian/ - Linux:
~/.local/share/melian/
config.yaml: Server & LLM
The main config file. Controls the server, LLM connection, vector database, and embeddings.
server:
port: 3000
host: "127.0.0.1"
tls: # Optional: HTTPS
cert: ~/.melian/certs/tls.crt
key: ~/.melian/certs/tls.key
llm:
baseUrl: "http://localhost:1234/v1" # OpenAI-compatible endpoint
maxRetries: 3
timeout: 30000 # ms
maxContextTokens: 131072
models:
preferred: # Model priority list
- "qwen2.5-14b-instruct"
complexity_routing: false # Route by query complexity
complexity_threshold: 0.5 # 0–1 score threshold
discovery_cache_ttl: 30 # seconds
fallback: # Optional: fallback provider
enabled: false
baseUrl: ""
apiKey: ""
qdrant:
url: "http://localhost:6333"
collection: "archival_memory"
embeddingDimensions: 768 # Must match embedding model
embeddings:
baseUrl: "http://localhost:1234/v1"
model: "nomic-embed-text-v1.5"
user: # Optional: identity context
name: "Your Name"
interests:
- software engineering
- Tolkien
selfHealing: # Optional
enabled: true
dryRun: false
maxFixesPerRun: 5
push: # Optional: Web Push
vapidPublicKey: "..."
vapidPrivateKey: "..."
vapidSubject: "mailto:[email protected]"email.yaml: Email Accounts
Each account has its own IMAP/SMTP credentials and permission tier.
accounts:
- name: personal
provider: gmail # gmail | protonmail | generic
email: [email protected]
oauth:
client_id: "872077090790-..."
client_secret: "GOCSPX-..."
refresh_token: "1//04zx90aF..."
imap:
host: imap.gmail.com
port: 993
tls: true
smtp:
host: smtp.gmail.com
port: 465
tls: true
send_enabled: true
check_interval: 30m
permissions:
autonomous: # Auto-send to these domains
- your-domain.com
blocked: # Never touch these
- irs.govPermission tiers: autonomous (auto-send), draft (approval required), everything else defaults to draft.
imessage.yaml: iMessage
Controls the iMessage watcher, contact modes, and self-chat configuration.
imessage:
enabled: true
pipeline: legacy # legacy | shadow | unified
poll_interval: 3 # seconds
session_timeout: 900 # seconds
max_reply_length: 500
self_chat:
identifier: "[email protected]"
watch:
- "+15555550100"
- "[email protected]"
contacts:
- identifier: "+15555550100"
name: "Mom"
mode: auto # auto | draft | notify | triage | spam
context: "Custom prompt for this contact..." # Optional custom prompt
unconstrained: false # Optional: relaxed guardrails
busy_auto_reply: false # Optional: reply when busyModes: auto (reply directly), draft (generate for approval via self-chat), notify (summarize without reply), triage (attention pipeline), spam (discard).
calendar.yaml: Google Calendar
enabled: true
default_account: personal # Must match an email account name
default_calendar: [email protected] # Google calendar ID
reminders:
morning_briefing: "8:00am"
pre_event_minutes: 15
skip_all_day: false
defaults:
duration_minutes: 60
reminder_minutes: 15
timezone: America/DenverCalendar uses the OAuth credentials from the matching email account. On first run with calendar enabled, Melian prints an authorization URL; open it, approve, and paste the code.
tasks.yaml: Obsidian & Remarkable
enabled: true
obsidian:
vault_path: "/Users/you/Documents/Vault"
tasks_file: tasks.md
remarkable: # Optional
notebooks:
- path: Work/Tasks
defaultTag: work
- path: Personal Tasks
defaultTag: personal
sync:
schedule: "0 * * * *" # Cron: sync every hour
followup:
schedule: "0 13 * * *" # Daily at 1pmgovee.yaml: Govee Smart Home
enabled: true
multicast_address: 239.255.255.250 # LAN discovery
discovery_timeout_ms: 3000
api_key: "ed812abf-..." # Optional: cloud API keyThe api_key enables cloud API fallback for devices that don't support LAN control (e.g., the H5179 hygrometer).
wiz.yaml: Wiz Lights
enabled: true
broadcast_address: 255.255.255.255 # LAN broadcast
discovery_timeout_ms: 2000Wiz is LAN-only. No cloud API.
mcp.json: MCP Servers
{
"servers": {
"duckduckgo": {
"type": "stdio",
"command": "uvx",
"args": ["duckduckgo-mcp-server"]
},
"custom-tool": {
"type": "stdio",
"command": "bun",
"args": ["run", "/path/to/server.ts"],
"env": {
"API_KEY": "..."
}
}
}
}MCP servers are started at boot and their tools appear alongside Melian's built-in tools.
personality.yaml: Tone & Personality
personality:
base:
tone: warm, thoughtful, concise
humor: dry, understated
verbosity: concise, lead with the answer
traits: |
- Proactive about remembering things
- Asks clarifying questions
- Respects the user's time
contexts: # Override per context
imessage:
tone: casual, conversational
verbosity: brief
email:
tone: professional but human
cron:
tone: efficient, informative
verbosity: only report what's noteworthyEnvironment Variables
Bun loads .env from the project root automatically. Environment variables override config.yaml values.
| Variable | Overrides | Description |
|---|---|---|
PORT |
server.port |
HTTP server port |
HOST |
server.host |
Bind address |
LLM_BASE_URL |
llm.baseUrl |
LLM API endpoint |
LLM_API_KEY |
llm.apiKey |
API key (blank for local) |
QDRANT_URL |
qdrant.url |
Qdrant server URL |
GOVEE_API_KEY |
govee.api_key |
Govee Cloud API key |
Complexity Routing
When models.complexity_routing is true, Melian scores each query before selecting a model. Scores below complexity_threshold use the first model in preferred; scores above use the next available model. The scorer is a lightweight feature extractor (no LLM call) that checks message length, turn count, code blocks, tool calls, and question count.