initial commit

This commit is contained in:
Florian Krebs
2026-05-04 20:49:45 +00:00
parent 7d205a68e4
commit 18d2360ad9
14 changed files with 1811 additions and 3 deletions
+25
View File
@@ -0,0 +1,25 @@
# NEXA - Minimalist .env Template
# Nur absolut essenzielle Secrets. Alles andere wird via #nexa:config kommandos konfiguriert & autodiscovered.
# MEMOS (Primary Interface)
MEMOS_HOST=http://memos.local:5230
MEMOS_API_KEY=your-access-token
# SAIA / LiteLLM (Intelligence)
SAIA_HOST=https://saia.example.com
SAIA_API_KEY=your-litellm-api-key
# NEXTCLOUD (Tasks, Mail, Calendar - alles wird autodiscovered)
NC_HOST=https://nextcloud.example.com
NC_USERNAME=nexa_automation
NC_APP_PASSWORD=your-app-password
# QDRANT (Vector Memory - Semantic Search / Dense Retrieval)
QDRANT_HOST=http://qdrant.local:6333
QDRANT_API_KEY=your-api-key
# GRAPHDB (Ontotext - Semantic Graph / SPARQL)
GRAPHDB_HOST=http://graphdb.local:7200
GRAPHDB_REPOSITORY=nexa_knowledge
GRAPHDB_USERNAME=nexa
GRAPHDB_PASSWORD=your-graphdb-password
+629
View File
@@ -0,0 +1,629 @@
# NEXA Deployment Guide
## 🚀 Architektur-Überblick
```
┌─────────────────────────────────────────────────┐
│ MEMOS (Port 5230) │
│ Interface, Voice-Input, Webhooks │
└────────────────────┬────────────────────────────┘
┌───────────┼───────────┐
↓ ↓ ↓
┌────────┐ ┌────────┐ ┌──────────┐
│ n8n │ │ SAIA │ │ Nextcloud│
│Logic │ │LiteLLM │ │Tasks/Mail│
│7879 │ │4000 │ │80/443 │
└────┬───┘ └────┬───┘ └─────┬────┘
│ │ │
┌────┴───────────┼────────────┴────┐
↓ ↓ ↓
┌─────────┐ ┌────────────┐ ┌──────────────┐
│ QDRANT │ │ GraphDB │ │ Obsidian │
│Vector │ │ (Ontotext) │ │(local sync) │
│6333 │ │7200 │ │ │
└─────────┘ └────────────┘ └──────────────┘
```
---
## 📋 Voraussetzungen
### Hardware
- **CPU**: Min. 4 Cores (empfohlen 8)
- **RAM**: Min. 16 GB (empfohlen 32 GB)
- **Storage**: Min. 100 GB SSD (für Datenbanken & Backups)
- **Network**: Stabile Verbindung, HTTPS-ready
### Software
- Docker & Docker Compose (Version 20.10+)
- Git
- Bash
- curl/wget
- `.env` Datei aus `.env.example` (mit Secrets gefüllt)
### Accounts & Credentials
- Nextcloud Instanz mit Admin-Zugang
- OpenAI API-Key (für SAIA/LiteLLM)
- Obsidian Vault Zugang lokalem Filesystem
---
## 🐳 Schritt 1: Docker Compose Setup
### 1.1 Repository klonen
```bash
git clone https://github.com/noheton/nexa.git
cd nexa/nexa-core
```
### 1.2 .env konfigurieren
```bash
cp .env.example .env
# WICHTIG: Alle Secrets eintragen!
# - MEMOS_API_KEY
# - SAIA_API_KEY
# - NC_APP_PASSWORD
# - QDRANT_API_KEY
# - GRAPHDB_PASSWORD
nano .env
```
### 1.3 docker-compose.yml erstellen
```bash
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
# ===== MEMOS (Interface) =====
memos:
image: neosmemo/memos:latest
container_name: nexa-memos
ports:
- "5230:5230"
volumes:
- ./data/memos:/root/.memos
environment:
MEMOS_PORT: "5230"
MEMOS_DRIVER: sqlite
MEMOS_DSN: /root/.memos/memos.db
networks:
- nexa
restart: unless-stopped
# ===== n8n (Workflow Engine) =====
n8n:
image: n8nio/n8n:latest
container_name: nexa-n8n
ports:
- "5678:5678"
volumes:
- ./data/n8n:/home/node/.n8n
- ./n8n-workflows:/workflows
environment:
N8N_BASIC_AUTH_ACTIVE: "true"
N8N_BASIC_AUTH_USER: "${N8N_USER:-admin}"
N8N_BASIC_AUTH_PASSWORD: "${N8N_PASSWORD:-nexa_secure_pass}"
N8N_HOST: "${N8N_DOMAIN:-localhost}"
N8N_PORT: "5678"
N8N_PROTOCOL: https
N8N_WEBHOOK_URL: "https://${N8N_DOMAIN:-localhost}/webhook"
GENERIC_TIMEZONE: Europe/Berlin
DB_TYPE: sqlite
DB_SQLITE_PATH: /home/node/.n8n/nexa.db
networks:
- nexa
restart: unless-stopped
depends_on:
- memos
# ===== QDRANT (Vector DB) =====
qdrant:
image: qdrant/qdrant:latest
container_name: nexa-qdrant
ports:
- "6333:6333"
volumes:
- ./data/qdrant:/qdrant/storage
environment:
QDRANT_API_KEY: "${QDRANT_API_KEY}"
networks:
- nexa
restart: unless-stopped
# ===== GraphDB (Ontotext SPARQL) =====
graphdb:
image: ontotext/graphdb:10-ee
container_name: nexa-graphdb
ports:
- "7200:7200"
volumes:
- ./data/graphdb:/opt/graphdb/data
- ./config/graphdb-init.ttl:/graphdb-import/import.ttl
environment:
GDB_JAVA_OPTS: "-Xmx8g -Xms4g"
GDB_PASSWORD: "${GRAPHDB_PASSWORD}"
networks:
- nexa
restart: unless-stopped
# ===== SAIA / LiteLLM Proxy (optional - wenn lokal) =====
# (Falls SAIA extern gehostet: nicht nötig)
networks:
nexa:
driver: bridge
volumes:
nexa_memos:
nexa_n8n:
nexa_qdrant:
nexa_graphdb:
EOF
```
---
## 🔧 Schritt 2: Individuelle Service-Konfiguration
### 2.1 MEMOS Initialisierung
```bash
# Nach memos startup (ca. 30s)
curl -X POST http://localhost:5230/api/v1/auth/signup \
-H "Content-Type: application/json" \
-d '{
"username": "nexa",
"password": "secure_password"
}'
# API-Token generieren
TOKEN=$(curl -X POST http://localhost:5230/api/v1/auth/signin \
-H "Content-Type: application/json" \
-d '{"username":"nexa","password":"secure_password"}' \
| jq -r '.data.token')
echo "MEMOS_API_KEY=$TOKEN" >> .env
```
### 2.2 Qdrant Konfiguration
```bash
# Warte auf Qdrant startup
sleep 10
# Erstelle Collection für Nexa
curl -X PUT "http://localhost:6333/collections/nexa_knowledge" \
-H "Content-Type: application/json" \
-H "api-key: ${QDRANT_API_KEY}" \
-d '{
"vectors": {
"size": 1536,
"distance": "Cosine"
},
"optimizers_config": {
"deleted_threshold": 0.2,
"vacuum_threshold": 0.5,
"default_segment_number": 5,
"max_segment_number": 30,
"memmap_threshold": 268435456,
"indexing_threshold": 20000,
"flush_interval_sec": 10,
"max_optimization_threads": 4
}
}'
echo "✅ Qdrant Collection erstellt"
```
### 2.3 GraphDB Initialisierung
```bash
# Warte auf GraphDB startup
sleep 15
# Erstelle Repository
GRAPHDB_URL="http://localhost:7200"
curl -X POST "$GRAPHDB_URL/rest/repositories" \
-H "Content-Type: application/json" \
-u "admin:${GRAPHDB_PASSWORD}" \
-d '{
"id": "nexa_knowledge",
"title": "Nexa Knowledge Graph",
"type": "free",
"params": {
"baseURL": {
"label": "Base URL",
"value": "http://nexa.local/"
},
"defaultLanguage": {
"label": "Default language",
"value": "en"
}
}
}'
echo "✅ GraphDB Repository erstellt"
```
### 2.4 n8n Workflows Import
```bash
# Warte auf n8n startup (ca. 60s)
sleep 60
# Importiere Discovery Workflow
curl -X POST http://localhost:5678/api/v1/workflows \
-H "Content-Type: application/json" \
-u "admin:${N8N_PASSWORD}" \
-d @n8n-workflows/phase-1/1_1_discovery_workflow.json
# Importiere Memos Bridge
curl -X POST http://localhost:5678/api/v1/workflows \
-H "Content-Type: application/json" \
-u "admin:${N8N_PASSWORD}" \
-d @n8n-workflows/phase-1/1_2_memos_bridge_v2.json
# Importiere Email Butler
curl -X POST http://localhost:5678/api/v1/workflows \
-H "Content-Type: application/json" \
-u "admin:${N8N_PASSWORD}" \
-d @n8n-workflows/phase-2/2_1_email_butler.json
echo "✅ n8n Workflows importiert"
```
---
## 🚀 Schritt 3: Deployment starten
### 3.1 Docker Compose Up
```bash
docker-compose up -d
# Logs überwachen
docker-compose logs -f
```
### 3.2 Health Checks
```bash
# Health Check Script
check_service() {
local name=$1
local url=$2
echo "Checking $name..."
if curl -s "$url" > /dev/null; then
echo "$name: OK"
else
echo "$name: FAILED"
fi
}
sleep 30
check_service "Memos" "http://localhost:5230"
check_service "n8n" "http://localhost:5678"
check_service "Qdrant" "http://localhost:6333/health"
check_service "GraphDB" "http://localhost:7200/health"
echo ""
echo "🎯 Service URLs:"
echo " - Memos: http://localhost:5230"
echo " - n8n: http://localhost:5678"
echo " - Qdrant: http://localhost:6333"
echo " - GraphDB: http://localhost:7200"
```
---
## ⚙️ Schritt 4: Konfiguration via Command System
### 4.1 Discovery-Workflow triggern
```bash
# In Memos Memo erstellen:
# #nexa:config
# Oder via API:
curl -X POST http://localhost:5678/api/v1/workflows/run \
-H "Content-Type: application/json" \
-u "admin:${N8N_PASSWORD}" \
-d '{
"workflowId": "discovery-workflow-id"
}'
```
### 4.2 Automatische Konfiguration laden
Nach erfolgreichem Discovery werden Nextcloud-Listen-IDs:
```bash
# Es wird automatisch aktualisiert in:
# - Qdrant: als Metadata
# - n8n: als Umgebungsvariablen
# - Memos: als Confirmation-Memo
```
---
## 🔐 Schritt 5: Reverse Proxy Setup (Production)
### 5.1 Nginx Configuration
```bash
cat > /etc/nginx/sites-available/nexa.conf << 'EOF'
upstream memos_backend {
server localhost:5230;
}
upstream n8n_backend {
server localhost:5678;
}
upstream qdrant_backend {
server localhost:6333;
}
upstream graphdb_backend {
server localhost:7200;
}
server {
listen 443 ssl http2;
server_name nexa.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/nexa.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/nexa.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# MEMOS
location / {
proxy_pass http://memos_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# n8n
location /n8n/ {
proxy_pass http://n8n_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Qdrant (nur intern, wenn nötig)
location /qdrant/ {
allow 10.0.0.0/8;
deny all;
proxy_pass http://qdrant_backend/;
}
# GraphDB (nur intern)
location /graphdb/ {
allow 10.0.0.0/8;
deny all;
proxy_pass http://graphdb_backend/;
}
# Webhook endpoint für externe Services
location /webhook/ {
proxy_pass http://n8n_backend/webhook/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
server {
listen 80;
server_name nexa.yourdomain.com;
return 301 https://$server_name$request_uri;
}
EOF
# Aktivieren
sudo ln -s /etc/nginx/sites-available/nexa.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
---
## 📊 Schritt 6: Monitoring & Backups
### 6.1 Backup Script
```bash
cat > scripts/backup_nexa.sh << 'EOF'
#!/bin/bash
BACKUP_DIR="./backups/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
echo "🔄 Backing up Nexa..."
# Backup Datenbanken
docker-compose exec -T qdrant tar czf - /qdrant/storage > "$BACKUP_DIR/qdrant.tar.gz"
docker-compose exec -T graphdb tar czf - /opt/graphdb/data > "$BACKUP_DIR/graphdb.tar.gz"
docker-compose exec -T memos tar czf - /root/.memos > "$BACKUP_DIR/memos.tar.gz"
docker-compose exec -T n8n tar czf - /home/node/.n8n > "$BACKUP_DIR/n8n.tar.gz"
# Backup Configs
cp .env "$BACKUP_DIR/.env.backup"
cp docker-compose.yml "$BACKUP_DIR/docker-compose.yml.backup"
echo "✅ Backup completed: $BACKUP_DIR"
# Optional: zu S3 hochladen
# aws s3 sync "$BACKUP_DIR" s3://nexa-backups/$(date +%Y%m%d)/
EOF
chmod +x scripts/backup_nexa.sh
# Tägliches Backup (cron)
echo "0 2 * * * /path/to/nexa/scripts/backup_nexa.sh" | crontab -
```
### 6.2 Health Check Monitoring
```bash
cat > scripts/monitor_nexa.sh << 'EOF'
#!/bin/bash
# Monitore Service Status
SERVICES=("memos" "n8n" "qdrant" "graphdb")
for service in "${SERVICES[@]}"; do
if docker-compose ps $service | grep -q "Up"; then
echo "✅ $service: Running"
else
echo "❌ $service: DOWN"
# Restart bei Fehler
docker-compose restart $service
fi
done
EOF
chmod +x scripts/monitor_nexa.sh
# Alle 5 Minuten
echo "*/5 * * * * /path/to/nexa/scripts/monitor_nexa.sh" | crontab -
```
---
## 🧪 Schritt 7: Validation & Testing
### 7.1 End-to-End Test
```bash
cat > scripts/test_nexa.sh << 'EOF'
#!/bin/bash
echo "🧪 Testing Nexa Installation..."
# Test 1: Memos API
echo "Test 1: Memos API"
if curl -s http://localhost:5230/api/v1/auth/status | jq . > /dev/null 2>&1; then
echo "✅ Memos: OK"
else
echo "❌ Memos: FAILED"
fi
# Test 2: n8n API
echo "Test 2: n8n API"
if curl -s -u "admin:${N8N_PASSWORD}" http://localhost:5678/api/v1/workflows | jq . > /dev/null 2>&1; then
echo "✅ n8n: OK"
else
echo "❌ n8n: FAILED"
fi
# Test 3: Qdrant Collection
echo "Test 3: Qdrant Collection"
if curl -s -H "api-key: ${QDRANT_API_KEY}" http://localhost:6333/collections/nexa_knowledge | jq . > /dev/null 2>&1; then
echo "✅ Qdrant: OK"
else
echo "❌ Qdrant: FAILED"
fi
# Test 4: GraphDB Repository
echo "Test 4: GraphDB Repository"
if curl -s -u "admin:${GRAPHDB_PASSWORD}" http://localhost:7200/repositories | jq . > /dev/null 2>&1; then
echo "✅ GraphDB: OK"
else
echo "❌ GraphDB: FAILED"
fi
echo ""
echo "🎯 Create Test Memo:"
MEMO=$(curl -s -X POST http://localhost:5230/api/v1/memos \
-H "Authorization: Bearer ${MEMOS_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"content": "Test memo - #nexa:config"}' | jq -r '.id')
echo "✅ Test Memo created: $MEMO"
echo "Check Memos UI to verify webhook triggers Discovery"
EOF
chmod +x scripts/test_nexa.sh
./scripts/test_nexa.sh
```
---
## 🆘 Troubleshooting
### Problem: Memos kann sich nicht mit n8n verbinden
```bash
# Überprüfe n8n Webhook URL in Memos config
curl http://localhost:5678/webhook/memos
# Falls 404: Workflow ist nicht aktiviert/deployed
docker-compose logs n8n | grep webhook
```
### Problem: GraphDB startet nicht
```bash
# Memory-Limit erhöhen
docker-compose exec graphdb -e "GDB_JAVA_OPTS=-Xmx16g -Xms8g" restart
# Logs checken
docker-compose logs graphdb
```
### Problem: Qdrant Collection leer
```bash
# Überprüfe ob Memos Bridge Workflow läuft
docker-compose logs n8n | grep "memos_bridge"
# Manuell test
curl -X POST http://localhost:5230/api/v1/memos \
-H "Authorization: Bearer ${MEMOS_API_KEY}" \
-d '{"content": "- [ ] Test task #nexa"}'
```
---
## 📈 Next Steps
1.**Phase 1 deployt**: Memos Bridge & Discovery laufen
2.**Phase 2 aktivieren**: Email Butler (wenn Nextcloud Mail konfiguriert)
3.**Phase 3 setup**: Qdrant Embedding Pipeline
4.**Phase 4 integration**: GraphRAG Queries testen
5.**Phase 5**: Home Assistant Voice Integration
---
## 📞 Support & Debugging
```bash
# Alle Logs in eine Datei sammeln
docker-compose logs > deployment_logs.txt
# Spezifischer Service
docker-compose logs n8n --tail 100
# Wiederherstellung aus Backup
./scripts/restore_nexa.sh backup/2024-01-15_120000/
```
---
**Deployment abgeschlossen! Nexa ist nun betriebsbereit.** 🚀
+11 -3
View File
@@ -1,5 +1,13 @@
Name: Nexa
Rolle: Zentrales Nervensystem
Eigenschaften: Präzise, proaktiv, leicht trocken-humorvoll.
Aufgabe: Überwacht 20 E-Mails/Tag, kuratiert Memos, schlägt Timeboxing für Nextcloud vor und verwaltet das Wissen in Qdrant.
Instruktion: Wenn der User eine Notiz in Memos mit "Nexa:" beginnt, analysiere den Kontext und antworte kurz und bündig.
Aufgabe: Überwacht E-Mails, kuratiert Wissen und plant Tasks.
WICHTIGE LOGIK:
- Tasks müssen strikt zwischen "Work" und "Personal" unterschieden werden.
- Analysiere den Inhalt eines Memos: Bezieht es sich auf professionelle Projekte (Obsidian Context) -> Work List. Bezieht es sich auf Haushalt, Einkäufe oder Freizeit -> Personal List.
- Im Zweifel: Nachfragen via Memos-Kommentar.
KONSUM-LOGIK:
- Wenn ein Link von Karakeep kommt, analysiere das Produkt.
- Haushaltswaren/Verbrauchsgüter -> NC_LIST_ID_SHOPPING.
- Besondere Wünsche/Hobby/Technik -> NC_LIST_ID_WISHES.
+19
View File
@@ -0,0 +1,19 @@
# Nexa Klassifizierungs-Logik
Dieser Fragebogen dient der Feinabstimmung von SAIA, um Tasks korrekt zu routen.
### A. Schlüsselwörter & Projekte (Arbeit)
*Welche Begriffe triggern zwingend die Work-Liste?*
- [ ] Projekt-Namen (z.B. "Nexa-Core", "Infrastruktur-Audit")
- [ ] Rollenspezifische Begriffe ("Meeting", "Report", "Deadline")
- [ ] Tools, die nur im Job vorkommen.
### B. Ausschlusskriterien (Privat)
*Was darf niemals in die Work-Liste?*
- [ ] Lebensmittel, Rezepte, Haushalt.
- [ ] Bluesky-Input (sofern nicht explizit als Recherche markiert).
- [ ] Finanz-Mails (Privat-Bank).
### C. Umgang mit Unschärfe
- **Default:** Wenn < 70% Sicherheit -> `Personal`.
- **Interaktion:** Nexa erstellt einen Kommentar: "In welche Liste soll das? [W/P]".
+27
View File
@@ -0,0 +1,27 @@
# NEXA Infrastructure Manifest
services:
memos:
url: "${MEMOS_URL}"
auth_type: "Bearer Token"
features: ["webhooks", "comments_api"]
n8n:
environment: "LXC Docker"
backup_path: "./n8n-workflows"
ai_gateway:
provider: "LiteLLM / SAIA"
model_primary: "gpt-4-o" # oder dein lokales Modell
model_embeddings: "text-embedding-3-small"
storage:
qdrant: "http://localhost:6333"
s3_provider: "Custom S3"
nextcloud:
url: "${NC_URL}"
lists:
work: "ID_COMING_SOON"
personal: "ID_COMING_SOON"
calendars:
work: "CAL_ID_WORK"
personal: "CAL_ID_PERSONAL"
+16
View File
@@ -0,0 +1,16 @@
# Spezifikation: Phase 2 - Task-Router
## 1. Trigger
* **Typ:** Webhook (Memos)
* **Filter:** `payload.content` enthält `- [ ]` ODER `#todo`.
## 2. Intelligence Node (SAIA)
* **Prompt:** "Analysiere folgende Notiz. Gib als JSON zurück: { "context": "work" | "personal", "summary": "string", "urgency": 1-5 }."
* **Kontext-Zufuhr:** Lade die letzten 5 Obsidian-Projekttitel aus Qdrant als Referenz hoch.
## 3. Switch Node
* **Route 1:** Context == "work" -> Nextcloud Create Task (List ID: Work).
* **Route 2:** Context == "personal" -> Nextcloud Create Task (List ID: Personal).
## 4. Feedback Loop
* Poste die Task-ID und den gewählten Kontext als Kommentar unter das Original-Memo.
+131
View File
@@ -0,0 +1,131 @@
# Nexa Command System - #nexa:command
Nexa "hört" auf folgende Kommandos in Memos-Kommentaren oder als Memo-Inhalt mit Hashtag:
## 🔧 Konfigurations-Kommandos
### #nexa:config
- **Beschreibung**: Triggert Autodiscovery aller Services (Nextcloud, Qdrant, etc.)
- **Antwort**: Postet Report mit gefundenen Konfigurationen
- **Speichert**: Settings in DynoDB / Qdrant Metadata (nicht in .env)
```
#nexa:config
```
**Beispiel-Response:**
```
✅ Autodiscovery abgeschlossen:
- Nextcloud Listen: Work (id=123), Personal (id=456), Shopping (id=789), Wishes (id=1011)
- Qdrant Collection: nexa_knowledge (1536 dims, Cosine)
- Nextcloud Mail: 3 Accounts erkannt (Office 365, Gmail, Privat)
```
### #nexa:status
- **Beschreibung**: Zeigt den aktuellen System-Status
- **Antwort**: Uptime, verbundene Systeme, Memory-Stats
```
#nexa:status
```
### #nexa:sync-obsidian
- **Beschreibung**: Triggert sofortige Synchronisierung mit Obsidian Vault
- **Optional Parameter**: `--vault=/path/to/vault` oder `--force` für Neuindexierung
```
#nexa:sync-obsidian --force
```
## 📊 Memory & RAG Kommandos
### #nexa:ask [question]
- **Beschreibung**: Semantische Suche in Qdrant (RAG) basierend auf Obsidian + Memos
- **Antwort**: Top-3 Treffer mit Quellen
```
#nexa:ask Wie implementierten wir das JWT-Middleware-Pattern?
```
### #nexa:learn [topic]
- **Beschreibung**: Explizit neue Information in Qdrant speichern (mit Tags)
- **Optional**: `--tag=architecture` `--source=obsidian/notes/arch.md`
```
#nexa:learn Das Routing-Schema unterscheidet Work vs. Personal via SAIA-Kontext-Analyse --tag=architecture
```
## 🎯 Workflow-Kommandos
### #nexa:route-test [text]
- **Beschreibung**: Testet die Klassifizierung (Work/Personal) ohne Task zu erstellen
```
#nexa:route-test Muss morgen die Präsentation für den Client fertigstellen
```
**Response:**
```
💬 Klassifizierung (Test-Mode):
- Kontext: WORK
- Vertrauen: 0.95
- Begründung: "Client-Präsentation → professioneller Kontext"
```
### #nexa:email-digest
- **Beschreibung**: Triggert sofortige Email-Zusammenfassung (sonst tägl. 7 Uhr)
```
#nexa:email-digest
```
## 🔐 Admin-Kommandos
### #nexa:reset-config
- **Beschreibung**: Löscht gespeicherte Konfiguration, triggert neuen Autodiscovery
- **Warnung**: Alle benutzerdefinierten Settings werden vergessen
```
#nexa:reset-config
```
### #nexa:export-state
- **Beschreibung**: Exportiert aktuellen State als JSON (für Backups)
```
#nexa:export-state
```
---
## 📝 Implementierung in n8n
Ein **Command Parser** läuft immer mit:
1. **Memos Webhook** empfängt alle Memos
2. **Regex Check**: Sucht nach `#nexa:command`
3. **Router**: Versendet an entsprechenden n8n-Workflow
4. **Antwort**: Postet Reply als Kommentar/Edit
**Command Parser Regex:**
```
^#nexa:(\w+)(?:\s+([^\n]*?))?(?:$|\s*--)
```
Extrahiert: `[command, parameters]`
---
## 🎛️ Dynamische Konfigurationsspeicherung
Statt .env zu editieren:
1. **First Run**: `#nexa:config` speichert zu lokalen Metadata
2. **Speich-Ziel**:
- **Primary**: Qdrant Metadata (als `_config` Namespace)
- **Fallback**: `nexa-core/config/runtime_config.json` (gitignored)
- **Notfall**: .env (nur initial)
3. **Reload-Logik**: Bei jedem Workflow-Start werden Settings aus Qdrant geladen
Das macht Nexa vollständig selbstständig nach dem initialem Setup!
+273
View File
@@ -0,0 +1,273 @@
# GraphRAG für Nexa - Strukturelles Wissen & Beziehungen
## 🧠 Zwei-Säulen-Memory-Architektur
Nexa nutzt zwei komplementäre Speicher für vollständiges Verständnis:
### Säule 1: Qdrant (Vector DB) - Semantische Ähnlichkeit
- **Frage**: "Was ist ähnlich, was ist relevant?"
- **Speichert**: Embeddings von Memos, Obsidian-Notes, E-Mail-Digests
- **Suche**: Cosine Similarity über 1536-dimensionale Vektoren
- **Antwort**: Top-k semantisch ähnliche Dokumente (RAG)
**Beispiel:**
```
Frage: "Wie war die JWT-Implementation in unserem letzten Projekt?"
→ Qdrant findet: 10 ähnliche Snippets → Prompt-Context
```
### Säule 2: Neo4j (Graph DB) - Strukturelle Beziehungen
- **Frage**: "Wer ist beteiligt? Was hängt davon ab? Wie ist es organisiert?"
- **Speichert**:
- **Knoten (Nodes)**: Projects, Tasks, People, Technologies, Dates
- **Kanten (Relationships)**: `USES`, `OWNS`, `DEPENDS_ON`, `MENTIONS`, `CREATED_BY`
- **Suche**: Cypher-Queries über Netzwerk-Topologie
- **Antwort**: Pfade, zentrale Knoten, Abhängigkeitsanalyse
**Beispiel:**
```cypher
MATCH (tech:Technology)-[USES*]->*(service:Service),
(tech)-[:MENTIONED_IN]->(task:Task)
WHERE tech.name = "JWT" AND task.status = "DONE"
RETURN tech, service, task
```
---
## 📊 Neo4j Schema für Nexa
### Node-Typen
```cypher
(Project {name, startDate, status, priority})
(Task {title, description, status, urgency: 1-5, context: "work"|"personal"})
(Person {name, role, email})
(Technology {name, category: "framework"|"language"|"tool"})
(Topic {name, domain})
(File {path, type, lastModified})
(Note {id, content_hash, created_at})
```
### Relationship-Typen
| Beziehung | Von | Zu | Beschreibung |
|-----------|-----|-----------|-------------|
| `OWNS` | Person | Task/Project | Wer ist verantwortlich |
| `USES` | Task/Project | Technology | Welche Tech wird verwendet |
| `DEPENDS_ON` | Task/Project | Task/Project | Hängt davon ab |
| `CREATED_BY` | Note | Person | Autor einer Notiz |
| `MENTIONS` | Note/Task | Topic/Technology | Verweist auf |
| `RELATED_TO` | Task | Task | Zusammenhängend |
| `CHILD_OF` | Task | Project | Task gehört zu Project |
| `SCHEDULED_FOR` | Task | Date | Zeitliche Zuordnung |
---
## 🔄 Synchronisierungs-Flows
### 1. **Memos → Neo4j** (automatisch beim Erstellen)
Wenn ein Memo mit `- [ ]` erstellt wird:
```
Memo-Content: "Muss JWT-Middleware für Auth-Service refaktorieren"
SAIA extrahiert Entitäten:
- Technologie: "JWT-Middleware", "Auth-Service"
- Kontext: "work"
- Urgenz: 3
Erstelle in Neo4j:
- Task node (title="JWT-Middleware refaktorieren")
- Technology nodes: JWT, Auth-Service
- Relationships: Task -[USES]-> JWT, Task -[USES]-> Auth-Service
- Task -[CREATED_BY]-> Me (Person node)
```
### 2. **Obsidian → Neo4j** (#nexa:sync-obsidian)
Beim Sync von Obsidian-Vault:
```
File: obsidian/projects/nexa-deployment.md
Header Struktur wird zu Graph:
- Project "Nexa Deployment"
- Technologies: Docker, n8n, Nextcloud
- Dependencies: "requires infrastructure setup"
Erstelle:
- Project nodes mit Metadata
- Hierarchie: Project -[CONTAINS]-> Task -[USES]-> Technology
```
### 3. **Nextcloud Tasks → Neo4j** (bidirektional)
Wenn Task in Nextcloud erstellt/gelöcht:
```
Nextcloud Task: "Review JWT implementation" (in Work-List)
n8n Trigger erkennt Änderung
Update Neo4j:
- Task node status = "active"
- Task -[DEPENDS_ON]-> related Task (falls Beziehung existiert)
```
---
## 🤖 GraphRAG Queries - Praktische Anwendungen
### Query 1: "Zeige alle offenen Tasks, die JWT betreffen"
```cypher
MATCH (tech:Technology {name:"JWT"}),
(tech)-[:MENTIONED_IN|USES]-(task:Task),
(task)-[:CHILD_OF]->(project:Project)
WHERE task.status IN ["NEEDS-ACTION", "IN-PROGRESS"]
RETURN project.name, task.title, task.urgency
ORDER BY task.urgency DESC
```
### Query 2: "Von welchen Tasks/Projects hängt Auth-Service ab?"
```cypher
MATCH path = (dependent)-[:DEPENDS_ON*]->(service:Technology {name:"Auth-Service"})
RETURN [x IN nodes(path) | x.name], length(path)
ORDER BY length(path) ASC
```
### Query 3: "Wer arbeitet an diesem Projekt und auf welchen Technologien?"
```cypher
MATCH (person:Person)-[:OWNS]->(task:Task)-[:CHILD_OF]->(project:Project {name:"Nexa"}),
(task)-[:USES]->(tech:Technology)
RETURN person.name, collect(task.title), collect(tech.name)
```
### Query 4: "Was ist seit gestern neu an Technologien/Themen gelernt?"
```cypher
MATCH (note:Note)-[:MENTIONS]->(topic:Topic)
WHERE note.created_at > datetime.now() - duration('P1D')
RETURN DISTINCT topic.name, count(note) as mentions
ORDER BY mentions DESC
```
---
## 🔗 GraphRAG-Enhanced Answer Generation
Wenn Nutzer `#nexa:ask` fragt:
```
#nexa:ask Wie stehen die JWT-Tasks zur Deadline?
┌─────────────┴──────────────┐
↓ ↓
[Qdrant] [Neo4j]
Semantic Structural
Search Search
↓ ↓
Top-3 Notes (JWT)-[DEPENDS_ON]
mit ähnlichem → Connected Tasks
Kontext → Critical Path
↓ ↓
Combine: Merge + Rank
System Prompt (SAIA):
"Basierend auf diesem Wissen und diesen Abhängigkeiten, beantworte:"
Response mit:
- Semantisch relevanter Information (Qdrant)
- Strukturellem Kontext (Neo4j)
- Abhängigkeitsanalyse
```
---
## 🛠️ n8n Integration (Phase 3)
### Workflow: Graph-Sync Trigger
```
[Memos Webhook]
[Parse Content]
[SAIA: Extract Entities]
├→ Tasks, Projects, Technologies, People
└→ Relationships (uses, depends_on, mentions)
[Neo4j: Create/Update Nodes & Relationships]
[Index in Qdrant Metadata: Add graph_node_id]
```
### Workflow: Question Router
```
[#nexa:ask Query]
[Parallel Search]
├→ [Qdrant: Vector Similarity]
├→ [Neo4j: Graph Query (auto-generated Cypher)]
└→ [Rank & Merge Results]
[SAIA: Generate Answer with Context]
```
---
## 📋 Commands für Graph-Management
### #nexa:graph-status
Zeigt Statistiken der Graph-DB
```
#nexa:graph-status
```
Response:
```
📊 Neo4j Graph Status:
- Nodes: 342 (Tasks: 145, Projects: 12, Technologies: 52, Notes: 133)
- Relationships: 1,247
- Density: 0.34
- Most Connected: "JWT-Middleware" (27 connections)
```
### #nexa:graph-trace [entity]
Zeigt alle Verbindungen für eine Entität
```
#nexa:graph-trace JWT-Middleware
```
### #nexa:graph-rebuild
Rekonstruiert Graph aus Obsidian + Memos (einmalig, danach inkrementell)
```
#nexa:graph-rebuild --force
```
---
## 🚀 Phase 3 Roadmap (Graph Integration)
- [ ] **3.1 Neo4j Setup**: Collection, Constraints, Indexes
- [ ] **3.2 Graph-Sync Workflow**: Automatische Entity Extraction
- [ ] **3.3 GraphRAG Pipeline**: Kombinierte Qdrant + Neo4j Queries
- [ ] **3.4 Dependency Tracker**: Warnt vor zirkulären Dependencies
- [ ] **3.5 Critical Path Analysis**: Berechnet längsten Weg im Graph
---
## 💡 Warum zwei DBs?
| Szenario | Qdrant | Neo4j | Kombi |
|----------|--------|-------|-------|
| "Welche Note war ähnlich?" | ✅ Best | ❌ | Qdrant |
| "Welche Tasks blockieren diesen Task?" | ❌ | ✅ Best | Neo4j |
| "Erkläre mir dieses Projekt umfassend" | ✅ Context | ✅ Structure | **Beide!** |
| "Finde alle JWT-bezogenen offenen Work" | ✅ Semantic | ✅ Crisp | **Beide!** |
Kombiniert: **Vollständiges Verständnis** statt nur Suchindex oder nur Struktur.
+79
View File
@@ -0,0 +1,79 @@
# 🔗 Nexa Integrations-Matrix (V2)
Diese Matrix definiert die logische Trennung zwischen privaten und beruflichen Datenströmen sowie die Anbindung der Infrastruktur.
---
## 1. Die Dualität: Arbeit vs. Privat
Nexa muss strikt zwischen zwei Kontexten unterscheiden, da die Datenquellen variieren:
### A. Bereich: ARBEIT (Work)
* **Sichtbarkeit:** Eingeschränkt (kein Zugriff auf Arbeits-E-Mails oder Firmen-Server).
* **Datenquellen:**
* Separate Nextcloud Task-Liste (`Work_Tasks`).
* Separater Nextcloud Kalender (`Work_Calendar`).
* Memos mit Tag `#work` oder semantischer Erkennung.
* **Nexa-Fokus:** Timeblocking, Fokus-Zeiten, Vorbereitung von Meetings basierend auf Obsidian-Notizen.
### B. Bereich: PRIVAT (Personal)
* **Sichtbarkeit:** Vollständig.
* **Datenquellen:**
* Nextcloud Task-Liste (`Personal_Tasks`).
* Haupt-Kalender.
* E-Mails (IMAP - 20 Mails/Tag).
* Bluesky, RSS, Karakeep.
* **Nexa-Fokus:** Automatisierung des Alltags, Kuratierung von Wissen, E-Mail-Management.
### Karakeep & Shopping-Logik
* **Schnittstelle:** Karakeep API / RSS.
* **Listen-Routing:**
* **Einkaufsliste:** Automatischer Sync für Items mit hoher Frequenz (Lebensmittel, Drogerie).
* **Wunschliste:** Speicherort für "Entdeckungen". Nexa fügt bei Wunschlisten-Items automatisch den aktuellen Preis und eine kurze SAIA-Zusammenfassung hinzu ("Warum du das speichern wolltest").
* **Review-Cycle:** Einmal im Monat fragt Nexa bei Wunschlisten-Items nach: "Immer noch interessiert oder kann das weg?"
---
## 2. Kern-Infrastruktur (The Brain & Spine)
| Komponente | Rolle im System |
| :--- | :--- |
| **Memos** | Zentraler Input (Drafts, Ideen, schnelle Tasks). Schnittstelle für Nexa-Antworten. |
| **n8n** | Logik-Engine. Führt die Klassifizierung Arbeit vs. Privat durch. |
| **SAIA (LiteLLM)** | Entscheidet anhand des Inhalts, in welchen Kalender/Liste ein Eintrag gehört. |
| **Qdrant** | Langzeitgedächtnis. Speichert Projektwissen (Work) und privates Wissen getrennt. |
| **Arcane** | Verwaltung der Docker-Container (n8n, Qdrant, Memos). |
---
## 3. Spezifische Datenflüsse & Logik
### Task-Routing (Das Gehirn-Filter)
1. **Input:** Neues Memo oder Spracheingabe.
2. **Analyse:** SAIA prüft: "Ist das Business oder Privat?"
3. **Routing:** * `Arbeits-Kontext` -> Eintrag in `Work_Tasks`.
* `Privat-Kontext` -> Eintrag in `Personal_Tasks`.
4. **Timeboxing:** Nexa scannt den `Work_Calendar` auf Lücken und schlägt Slots für `Work_Tasks` vor.
### E-Mail Management (Nur Privat)
* Nexa filtert die 20 täglichen Privat-Mails.
* Wichtige private Termine werden in den privaten Kalender extrahiert.
* Newsletter landen als Zusammenfassung in Memos.
### Wissens-Management (Obsidian & Qdrant)
* Obsidian-Notizen (Arbeit & Privat) werden in Qdrant indiziert.
* Nexa nutzt dieses Wissen, um Arbeits-Tasks besser zu beschreiben, auch wenn sie keinen Zugriff auf die Arbeits-E-Mails hat.
---
## 4. Monitoring & Hardware (Home Assistant)
| Dienst | Reiz-Typ | Nexa-Reaktion |
| :--- | :--- | :--- |
| **Home Assistant** | Voice / Sensorik | Nexa nimmt Sprachbefehle entgegen und meldet Alarme (Haus). |
| **Proxmox** | Stabilität | Meldung an Nexa bei Hardware-Problemen. |
| **S3 Server** | Archiv | Langzeit-Backups der Wissensdatenbank. |
---
## 5. Ergänzungen für das Setup (Vorschlag)
* **Zwei-Faktor-Klassifizierung:** Einführung von expliziten Tags (`#w` für Work, `#p` für Privat) für Grenzfälle, in denen SAIA den Kontext nicht eindeutig bestimmen kann.
+80
View File
@@ -0,0 +1,80 @@
📑 Projekt Nexa: Scoping & Vision
1. Vision & Zielsetzung
Nexa ist als "Zentrales Nervensystem" konzipiert. Das Ziel ist die Schaffung einer intelligenten Middleware zwischen Input-Quellen (Memos, E-Mail, RSS), Wissensspeichern (Obsidian, Qdrant) und Organisations-Tools (Nextcloud Calendar/Tasks).
Kernziele:
Kognitive Entlastung: Nexa sortiert, filtert und schlägt vor, anstatt nur zu speichern.
Kontext-Trennung: Saubere, KI-gestützte Unterscheidung zwischen Privat und Arbeit.
Single Point of Interaction: Memos fungiert als primäre Schnittstelle ("The Voice & Ear").
Wissens-Synergie: Verknüpfung von flüchtigen Memos mit tiefem Wissen in Obsidian via semantischer Suche.
2. Abgrenzung (Scope)
Um die Komplexität beherrschbar zu halten, wird das Projekt wie folgt abgegrenzt:
In-Scope (Was Nexa tut):
Triaging: Klassifizierung von 20 Mails/Tag (Privat).
Task-Routing: Verteilung von Aufgaben auf die richtige Nextcloud-Liste (Work vs. Personal).
Scheduling: Vorschlagen von Timeboxen im Arbeitskalender.
Memory: Aufbau eines Langzeitgedächtnisses in Qdrant.
Monitoring: Aggregation kritischer IT-Alarme (Proxmox, Backrest).
Out-of-Scope (Was Nexa NICHT tut):
Kein direkter Zugriff auf Arbeits-IT (E-Mail/Server).
Keine aktive Bearbeitung von Dokumenten (außer Metadaten-Extraktion).
Keine Ersetzung von Spezial-UIs (z.B. Arcane oder Portainer für Container-Management).
3. Phasenmodell der Entwicklung
Der Aufbau erfolgt iterativ, um frühzeitig Mehrwert zu generieren.
Phase 1: Die Wirbelsäule (Konnektivität)
Fokus: Datenfluss zwischen Memos, n8n und SAIA.
Meilenstein: Nexa antwortet auf Memos und kann einfache Fragen beantworten.
Technik: Webhooks, REST-API, LiteLLM-Proxy.
Phase 2: Sensorik & Klassifizierung (The Senses)
Fokus: E-Mail-Filterung und das Routing-System.
Meilenstein: Nexa erkennt den Unterschied zwischen "Arbeits-Task" und "Privat-Task" ohne manuelle Tags.
Technik: IMAP-Node, n8n-Logic-Router, System-Prompts für Kontext-Analyse.
Phase 3: Das semantische Gedächtnis (The Brain)
Fokus: Anbindung von Qdrant und Obsidian.
Meilenstein: RAG (Retrieval Augmented Generation). Nexa beantwortet Fragen basierend auf deinen archivierten Notizen.
Technik: Embeddings-Pipeline, Qdrant-Vektor-Suche.
Phase 4: Proaktive Motorik (The Assistant)
Fokus: Kalender-Integration und Timeboxing.
Meilenstein: Nexa schlägt morgens proaktiv Fokus-Zeiten im Arbeitskalender vor.
Technik: Nextcloud CalDAV/CardDAV Integration, Optimierungs-Logik für freie Slots.
Phase 5: Physische Präsenz (Voice & Alarms)
Fokus: Home Assistant Voice & Monitoring-Integration.
Meilenstein: Nexa spricht über HA-Voice und meldet kritische Systemzustände proaktiv.
Technik: Home Assistant API, Wyoming Protocol, ntfy/Memos-Push.
4. Erfolgsmetriken
Woran merken wir, dass Nexa funktioniert?
Reduktion der Mail-Interaktion: Weniger Zeit in der Mail-App, mehr Fokus auf den Nexa-Digest.
Kalender-Füllrate: Fokus-Slots für Arbeit sind automatisch geblockt.
Such-Geschwindigkeit: Informationen aus alten Memos/Obsidian werden via #ask sofort gefunden.
@@ -0,0 +1,127 @@
{
"name": "Phase 1.1: Nextcloud Lists Discovery",
"description": "Discovers and maps Nextcloud task list IDs (Work, Personal, Shopping, Wishes) for environment configuration",
"nodes": [
{
"parameters": {},
"name": "Start",
"type": "n8n-nodes-base.start",
"typeVersion": 1,
"position": [50, 50]
},
{
"parameters": {
"method": "GET",
"url": "{{ $env.NC_HOST }}/remote.php/dav/lists/",
"authentication": "basicAuth",
"basicAuth": {
"user": "{{ $env.NC_USERNAME }}",
"password": "{{ $env.NC_APP_PASSWORD }}"
},
"responseFormat": "text"
},
"name": "Fetch Nextcloud Lists (WebDAV)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [250, 50],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "// Parse XML response from WebDAV\nconst xmlText = $input.item.json.body || '';\n\n// Extract list names and IDs from XML\nconst listRegex = /<d:href>(\\/remote\\.php\\/dav\\/lists\\/[^/]+\\/)<\\/d:href>[\\s\\S]*?<d:displayname>([^<]+)<\\/d:displayname>/g;\nconst lists = [];\nlet match;\n\nwhile ((match = listRegex.exec(xmlText)) !== null) {\n const href = match[1];\n const displayname = match[2];\n const listId = href.split('/').filter(p => p)[3]; // Extract ID from path\n lists.push({\n name: displayname,\n id: listId,\n url: href\n });\n}\n\n// Identify special lists by name patterns\nconst result = {\n discovery_timestamp: new Date().toISOString(),\n lists_found: lists,\n mapping: {\n work: lists.find(l => l.name.toLowerCase().includes('work'))?.id || 'NOT_FOUND',\n personal: lists.find(l => l.name.toLowerCase().includes('personal'))?.id || 'NOT_FOUND',\n shopping: lists.find(l => l.name.toLowerCase().includes('shopping'))?.id || 'NOT_FOUND',\n wishes: lists.find(l => l.name.toLowerCase().includes('wishes') || l.name.toLowerCase().includes('want'))?.id || 'NOT_FOUND'\n }\n};\n\nreturn result;"
},
"name": "Parse WebDAV Response",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [450, 50]
},
{
"parameters": {
"jsCode": "// Generate .env snippet for configuration\nconst mapping = $input.item.json.mapping;\nconst timestamp = new Date().toISOString();\n\nconst envSnippet = `# NEXA Nextcloud Lists - Discovered on ${timestamp}\nNC_LIST_ID_WORK=${mapping.work}\nNC_LIST_ID_PERSONAL=${mapping.personal}\nNC_LIST_ID_SHOPPING=${mapping.shopping}\nNC_LIST_ID_WISHES=${mapping.wishes}`;\n\nreturn {\n env_snippet: envSnippet,\n discovered_lists: $input.item.json.lists_found,\n ready_for_deployment: Object.values(mapping).every(v => v !== 'NOT_FOUND')\n};"
},
"name": "Generate .env Configuration",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [650, 50]
},
{
"parameters": {
"method": "POST",
"url": "{{ $env.MEMOS_HOST }}/api/v1/memos",
"authentication": "headerAuth",
"headerAuth": {
"Authorization": "Bearer {{ $env.MEMOS_API_KEY }}"
},
"sendBody": true,
"bodyParametersUi": "json",
"body": "{\n \"content\": \"🔍 **Nexa Discovery Report**\\n\\nNextcloud Lists gefunden:\\n\\n- **Work:** {{ $input.item.json.env_snippet.split('=')[1].split('\\\\n')[0] }}\\n- **Personal:** {{ $input.item.json.env_snippet.split('=')[2].split('\\\\n')[0] }}\\n- **Shopping:** {{ $input.item.json.env_snippet.split('=')[3].split('\\\\n')[0] }}\\n- **Wishes:** {{ $input.item.json.env_snippet.split('=')[4].split('\\\\n')[0] }}\\n\\nBitte diese IDs in .env eintragen.\",\n \"visibility\": \"PRIVATE\"\n}"
},
"name": "Post Report to Memos",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [850, 50],
"onError": "continueErrorOutput"
},
{
"parameters": {
"fromFile": true,
"fileName": "discovery_result.json"
},
"name": "Output Discovery Result",
"type": "n8n-nodes-base.set",
"typeVersion": 2,
"position": [1050, 50]
}
],
"connections": {
"Start": {
"main": [
[
{
"node": "Fetch Nextcloud Lists (WebDAV)",
"type": "main",
"index": 0
}
]
]
},
"Fetch Nextcloud Lists (WebDAV)": {
"main": [
[
{
"node": "Parse WebDAV Response",
"type": "main",
"index": 0
}
]
]
},
"Parse WebDAV Response": {
"main": [
[
{
"node": "Generate .env Configuration",
"type": "main",
"index": 0
}
]
]
},
"Generate .env Configuration": {
"main": [
[
{
"node": "Post Report to Memos",
"type": "main",
"index": 0
},
{
"node": "Output Discovery Result",
"type": "main",
"index": 0
}
]
]
}
}
}
@@ -0,0 +1,58 @@
{
"name": "Phase 1.2: Memos Bridge - Context Classifier & Task Router",
"description": "Receives Memos, classifies context (work/personal), and routes to appropriate Nextcloud list via SAIA",
"nodes": [
{
"parameters": {
"path": "memos"
},
"name": "Memos Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [50, 50]
},
{
"parameters": {
"jsCode": "// Extract and validate Memos webhook payload\nconst payload = $input.item.json;\n\n// Structure expected from Memos webhook\nconst memo = {\n id: payload.memo?.id || payload.id,\n content: payload.memo?.content || payload.content || '',\n createdAt: payload.memo?.createdAt || payload.createdAt,\n archived: payload.memo?.archived || false,\n // Detect if it's a task (contains - [ ] or #todo)\n isTask: /(- \\[ \\]|-\\s\\[ \\])|(#todo)/i.test(payload.memo?.content || payload.content || ''),\n isPriority: /(#urgent|#important|!important)/i.test(payload.memo?.content || payload.content || '')\n};\n\n// Ignore archived memos\nif (memo.archived) {\n throw new Error('Skipping archived memo');\n}\n\n// Only process if it contains task marker or #todo\nif (!memo.isTask) {\n throw new Error('Memo is not marked as task (missing - [ ] or #todo)');\n}\n\nreturn {\n memo: memo,\n shouldProcess: true,\n timestamp: new Date().toISOString()\n};"
},
"name": "Validate Memo Payload",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [250, 50]
},
{
"parameters": {
"method": "POST",
"url": "{{ $env.SAIA_HOST }}/v1/chat/completions",
"authentication": "headerAuth",
"headerAuth": {
"Authorization": "Bearer {{ $env.SAIA_API_KEY }}"
},
"sendBody": true,
"bodyParametersUi": "json",
"body": "{\n \"model\": \"{{ $env.SAIA_MODEL_CLASSIFICATION }}\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You are Nexa, a context classifier. Analyze the following task memo and classify it as either 'work' or 'personal'. Consider:\\n- Work context: Professional projects, client work, coding, architecture, development\\n- Personal context: Household, shopping, hobbies, health, leisure\\n\\nRespond with ONLY valid JSON: {\\\"context\\\": \\\"work\\\" | \\\"personal\\\", \\\"urgency\\\": 1-5, \\\"summary\\\": \\\"one-line summary\\\", \\\"confidence\\\": 0.0-1.0}\"\n },\n {\n \"role\": \"user\",\n \"content\": \"{{ $input.item.json.memo.content }}\"\n }\n ],\n \"temperature\": 0.3,\n \"max_tokens\": 200\n}"
},
"name": "SAIA: Classify Context",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [450, 50]
},
{
"parameters": {
"jsCode": "// Parse SAIA response\nconst response = $input.item.json;\nconst content = response.choices?.[0]?.message?.content || '{}';\n\nlet classification;\ntry {\n // Try to extract JSON from response\n const jsonMatch = content.match(/\\{[^{}]*\\}/);\n classification = JSON.parse(jsonMatch ? jsonMatch[0] : content);\n} catch (e) {\n console.error('Failed to parse SAIA response:', content);\n classification = { context: 'personal', confidence: 0 };\n}\n\nreturn {\n raw_response: content,\n classification: classification,\n timestamp: new Date().toISOString()\n};"
},
"name": "Parse Classification",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [650, 50]
},
{
"parameters": {
"conditions": {\n "options": [\n {\n "condition": "nodeOutputProperty",\n "value1": "={{ $node[\\\"Parse Classification\\\"].json.classification.context }}",\n "value2": "work"\n },\n {\n "condition": "nodeOutputProperty",\n "value1": "={{ $node[\\\"Parse Classification\\\"].json.classification.context }}",\n "value2": "personal"\n }\n ]\n }\n },\n "name": "Route by Context",\n "type": "n8n-nodes-base.switch",
n "typeVersion": 1,\n "position": [850, 50]\n },\n {\n "parameters": {\n "method": "POST",\n "url": "{{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_WORK }}/\",\n "authentication": "basicAuth",\n "basicAuth": {\n "user": "{{ $env.NC_USERNAME }}\",\n "password": "{{ $env.NC_APP_PASSWORD }}\"\n },\n "sendBody": true,\n "bodyParametersUi": "raw",\n "body": "BEGIN:VCALENDAR\\nVERSION:2.0\\nPRODID:-//Nexa//v1.0//EN\\nBEGIN:VTODO\\nUID:memo-{{ $node[\\\"Validate Memo Payload\\\"].json.memo.id }}-work\\nDTSTART:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.createdAt }}\\nDTSTAMP:{{ now().toISOString() }}\\nSUMMARY:{{ ($node[\\\"Parse Classification\\\"].json.classification.summary || $node[\\\"Validate Memo Payload\\\"].json.memo.content.substring(0, 50)).replace(/\\\"/g, \\\"\\\") }}\\nDESCRIPTION:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.content }}\\nPRIORITY:{{ Math.round((6 - $node[\\\"Parse Classification\\\"].json.classification.urgency)) }}\\nSTATUS:NEEDS-ACTION\\nEND:VTODO\\nEND:VCALENDAR\"\n },\n "name": "Create Task - Work List",\n "type": "n8n-nodes-base.httpRequest",\n "typeVersion": 3,\n "position": [1050, 50],\n "onError": "continueErrorOutput"\n },\n {\n "parameters": {\n "method": "POST",\n "url": "{{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_PERSONAL }}/\",\n "authentication": "basicAuth",\n "basicAuth": {\n "user": "{{ $env.NC_USERNAME }}\",\n "password": "{{ $env.NC_APP_PASSWORD }}\"\n },\n "sendBody": true,\n "bodyParametersUi": "raw",\n "body": "BEGIN:VCALENDAR\\nVERSION:2.0\\nPRODID:-//Nexa//v1.0//EN\\nBEGIN:VTODO\\nUID:memo-{{ $node[\\\"Validate Memo Payload\\\"].json.memo.id }}-personal\\nDTSTART:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.createdAt }}\\nDTSTAMP:{{ now().toISOString() }}\\nSUMMARY:{{ ($node[\\\"Parse Classification\\\"].json.classification.summary || $node[\\\"Validate Memo Payload\\\"].json.memo.content.substring(0, 50)).replace(/\\\"/g, \\\"\\\") }}\\nDESCRIPTION:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.content }}\\nPRIORITY:{{ Math.round((6 - $node[\\\"Parse Classification\\\"].json.classification.urgency)) }}\\nSTATUS:NEEDS-ACTION\\nEND:VTODO\\nEND:VCALENDAR\"\n },\n "name": "Create Task - Personal List",\n "type": "n8n-nodes-base.httpRequest",\n "typeVersion": 3,\n "position": [1050, 200],\n "onError": "continueErrorOutput"\n },\n {\n "parameters": {\n "jsCode": "// Create feedback message based on routing decision\nconst context = $node[\\\"Parse Classification\\\"].json.classification.context;\nconst urgency = $node[\\\"Parse Classification\\\"].json.classification.urgency;\nconst listName = context === 'work' ? 'Work' : 'Personal';\nconst emoji = context === 'work' ? '💼' : '🏠';\n\nreturn {\n feedback_content: `${emoji} Task routed to ${listName} list (Urgency: ${urgency}/5)`,\n classification: $node[\\\"Parse Classification\\\"].json.classification,\n memo_id: $node[\\\"Validate Memo Payload\\\"].json.memo.id\n};"
},
"name": "Prepare Feedback",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [1250, 50]
},\n {\n "parameters": {\n "method": "PATCH",\n "url": "{{ $env.MEMOS_HOST }}/api/v1/memos/{{ $node[\\\"Validate Memo Payload\\\"].json.memo.id }}\",\n "authentication": "headerAuth",\n "headerAuth": {\n "Authorization": "Bearer {{ $env.MEMOS_API_KEY }}\"\n },\n "sendBody": true,\n "bodyParametersUi": "json",\n "body": "{\n \\\"content\\\": \\\"{{ $node[\\\"Prepare Feedback\\\"].json.feedback_content }}\\n\\n_Processed by Nexa at {{ now().toLocaleString() }}_\\\"\n}"\n },\n "name": "Post Feedback to Memos",\n "type": "n8n-nodes-base.httpRequest",\n "typeVersion": 3,\n "position": [1450, 50],\n "onError": "continueErrorOutput"\n }\n ],\n "connections": {\n "Memos Webhook Trigger": {\n "main": [\n [\n {\n "node": "Validate Memo Payload",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Validate Memo Payload": {\n "main": [\n [\n {\n "node": "SAIA: Classify Context",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "SAIA: Classify Context": {\n "main": [\n [\n {\n "node": "Parse Classification",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Parse Classification": {\n "main": [\n [\n {\n "node": "Route by Context",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Route by Context": {\n "main": [\n [ { "node": "Create Task - Work List", "type": "main", "index": 0 } ],\n [ { "node": "Create Task - Personal List", "type": "main", "index": 0 } ]\n ]\n },\n "Create Task - Work List": {\n "main": [\n [\n {\n "node": "Prepare Feedback",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Create Task - Personal List": {\n "main": [\n [\n {\n "node": "Prepare Feedback",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Prepare Feedback": {\n "main": [\n [\n {\n "node": "Post Feedback to Memos",\n "type": "main",\n "index": 0\n }\n ]\n ]\n }\n }\n}\n
@@ -0,0 +1,143 @@
{
"name": "Phase 1.2: Memos Bridge - Context Classifier & Task Router",
"description": "Receives Memos webhook, classifies context (work/personal) via SAIA, routes to appropriate Nextcloud list",
"nodes": [
{
"parameters": {
"path": "memos"
},
"name": "Memos Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [50, 50]
},
{
"parameters": {
"jsCode": "const payload = $input.item.json;\nconst memo = {\n id: payload.memo?.id || payload.id,\n content: payload.memo?.content || payload.content || '',\n createdAt: payload.memo?.createdAt || payload.createdAt,\n archived: payload.memo?.archived || false,\n isTask: /(- \\[ \\]|#todo)/i.test(payload.memo?.content || payload.content || ''),\n isPriority: /(#urgent|#important)/i.test(payload.memo?.content || payload.content || '')\n};\nif (memo.archived || !memo.isTask) {\n throw new Error('Memo not a task');\n}\nreturn { memo, timestamp: new Date().toISOString() };"
},
"name": "Validate Memo Payload",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [250, 50]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.SAIA_HOST }}/v1/chat/completions",
"authentication": "headerAuth",
"headerAuth": {
"Authorization": "Bearer {{ $env.SAIA_API_KEY }}"
},
"sendBody": true,
"bodyParametersUi": "json",
"body": "{\n \"model\": \"{{ $env.SAIA_MODEL_CLASSIFICATION }}\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"Classify as work or personal. Respond: {context: work|personal, urgency: 1-5, summary: str}\"\n },\n {\n \"role\": \"user\",\n \"content\": \"{{ $input.item.json.memo.content }}\"\n }\n ],\n \"temperature\": 0.3\n}"
},
"name": "SAIA: Classify Context",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [450, 50]
},
{
"parameters": {
"jsCode": "const content = $input.item.json.choices?.[0]?.message?.content || '{}';\nlet classification;\ntry {\n const jsonMatch = content.match(/\\{[^{}]*\\}/);\n classification = JSON.parse(jsonMatch?.[0] || content);\n} catch (e) {\n classification = { context: 'personal', urgency: 2 };\n}\nreturn { classification, raw: content };"
},
"name": "Parse Classification",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [650, 50]
},
{
"parameters": {
"conditions": {
"options": [
{
"condition": "nodeOutputProperty",
"value1": "={{ $node[\"Parse Classification\"].json.classification.context }}",
"value2": "work"
},
{
"condition": "nodeOutputProperty",
"value1": "={{ $node[\"Parse Classification\"].json.classification.context }}",
"value2": "personal"
}
]
}
},
"name": "Route by Context",
"type": "n8n-nodes-base.switch",
"typeVersion": 1,
"position": [850, 50]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_WORK }}/",
"authentication": "basicAuth",
"basicAuth": {
"user": "={{ $env.NC_USERNAME }}",
"password": "={{ $env.NC_APP_PASSWORD }}"
},
"sendBody": true,
"bodyParametersUi": "raw",
"body": "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Nexa//v1.0//EN\nBEGIN:VTODO\nUID:{{ $node[\"Validate Memo Payload\"].json.memo.id }}\nSUMMARY:{{ $node[\"Parse Classification\"].json.classification.summary || $node[\"Validate Memo Payload\"].json.memo.content.substring(0, 50) }}\nDESCRIPTION:{{ $node[\"Validate Memo Payload\"].json.memo.content }}\nSTATUS:NEEDS-ACTION\nEND:VTODO\nEND:VCALENDAR"
},
"name": "Create Task - Work List",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [1050, 50]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_PERSONAL }}/",
"authentication": "basicAuth",
"basicAuth": {
"user": "={{ $env.NC_USERNAME }}",
"password": "={{ $env.NC_APP_PASSWORD }}"
},
"sendBody": true,
"bodyParametersUi": "raw",
"body": "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Nexa//v1.0//EN\nBEGIN:VTODO\nUID:{{ $node[\"Validate Memo Payload\"].json.memo.id }}\nSUMMARY:{{ $node[\"Parse Classification\"].json.classification.summary || $node[\"Validate Memo Payload\"].json.memo.content.substring(0, 50) }}\nDESCRIPTION:{{ $node[\"Validate Memo Payload\"].json.memo.content }}\nSTATUS:NEEDS-ACTION\nEND:VTODO\nEND:VCALENDAR"
},
"name": "Create Task - Personal List",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [1050, 200]
},
{
"parameters": {
"jsCode": "const context = $node[\"Parse Classification\"].json.classification.context;\nconst emoji = context === 'work' ? '💼' : '🏠';\nreturn { feedback: emoji + ' Task routed to ' + context + ' list', context };"
},
"name": "Prepare Feedback",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [1250, 50]
}
],
"connections": {
"Memos Webhook Trigger": {
"main": [[{ "node": "Validate Memo Payload", "type": "main", "index": 0 }]]
},
"Validate Memo Payload": {
"main": [[{ "node": "SAIA: Classify Context", "type": "main", "index": 0 }]]
},
"SAIA: Classify Context": {
"main": [[{ "node": "Parse Classification", "type": "main", "index": 0 }]]
},
"Parse Classification": {
"main": [[{ "node": "Route by Context", "type": "main", "index": 0 }]]
},
"Route by Context": {
"main": [
[{ "node": "Create Task - Work List", "type": "main", "index": 0 }],
[{ "node": "Create Task - Personal List", "type": "main", "index": 0 }]
]
},
"Create Task - Work List": {
"main": [[{ "node": "Prepare Feedback", "type": "main", "index": 0 }]]
},
"Create Task - Personal List": {
"main": [[{ "node": "Prepare Feedback", "type": "main", "index": 0 }]]
}
}
}
@@ -0,0 +1,193 @@
{
"name": "Phase 2.1: Email Butler - Nextcloud Mail Digest & Summarization",
"description": "Reads private emails from Nextcloud Mail, summarizes daily digest, posts to Memos",
"nodes": [
{
"parameters": {
"triggerType": "on",
"cronExpression": "0 7 * * *"
},
"name": "Daily Trigger (7 AM)",
"type": "n8n-nodes-base.cronTrigger",
"typeVersion": 1,
"position": [50, 50]
},
{
"parameters": {
"method": "GET",
"url": "={{ $env.NC_HOST }}/remote.php/dav/calendars/{{ $env.NC_USERNAME }}/personal/",
"authentication": "basicAuth",
"basicAuth": {
"user": "={{ $env.NC_USERNAME }}",
"password": "={{ $env.NC_APP_PASSWORD }}"
},
"responseFormat": "json"
},
"name": "Discover Nextcloud Mail Account",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [250, 50],
"onError": "continueErrorOutput"
},
{
"parameters": {
"method": "GET",
"url": "={{ $env.NC_HOST }}/ocs/v2.php/apps/mail/api/v1/accounts",
"authentication": "basicAuth",
"basicAuth": {
"user": "={{ $env.NC_USERNAME }}",
"password": "={{ $env.NC_APP_PASSWORD }}"
},
"responseFormat": "json",
"headers": {
"OCS-APIREQUEST": "true"
}
},
"name": "Fetch Mail Accounts from Nextcloud",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [450, 50],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "// Extract mail account IDs\nconst accounts = $input.item.json.ocs?.data || [];\nif (!Array.isArray(accounts) || accounts.length === 0) {\n throw new Error('No mail accounts found in Nextcloud');\n}\n\n// Find personal account (first private account)\nconst personalAccount = accounts.find(a => a.name?.toLowerCase().includes('privat') || a.name?.toLowerCase().includes('personal')) || accounts[0];\n\nreturn {\n account_id: personalAccount.id,\n account_name: personalAccount.name,\n email: personalAccount.email,\n total_accounts: accounts.length\n};"
},
"name": "Extract Personal Mail Account",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [650, 50]
},
{
"parameters": {
"method": "GET",
"url": "={{ $env.NC_HOST }}/ocs/v2.php/apps/mail/api/v1/accounts/{{ $node['Extract Personal Mail Account'].json.account_id }}/mailboxes",
"authentication": "basicAuth",
"basicAuth": {
"user": "={{ $env.NC_USERNAME }}",
"password": "={{ $env.NC_APP_PASSWORD }}"
},
"responseFormat": "json",
"headers": {
"OCS-APIREQUEST": "true"
}
},
"name": "Fetch Mailboxes",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [850, 50],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "// Get INBOX mailbox ID\nconst mailboxes = $input.item.json.ocs?.data || [];\nconst inbox = mailboxes.find(m => m.name === 'INBOX');\nif (!inbox) {\n throw new Error('INBOX not found');\n}\nreturn { mailbox_id: inbox.id };"
},
"name": "Get INBOX ID",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [1050, 50]
},
{
"parameters": {
"method": "GET",
"url": "={{ $env.NC_HOST }}/ocs/v2.php/apps/mail/api/v1/accounts/{{ $node['Extract Personal Mail Account'].json.account_id }}/mailboxes/{{ $node['Get INBOX ID'].json.mailbox_id }}/messages",
"authentication": "basicAuth",
"basicAuth": {
"user": "={{ $env.NC_USERNAME }}",
"password": "={{ $env.NC_APP_PASSWORD }}"
},
"responseFormat": "json",
"headers": {
"OCS-APIREQUEST": "true"
},
"qs": "from={{ $now.subtract(1, 'day').format('YYYY-MM-DD') }}&limit=20"
},
"name": "Fetch Today's Messages",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [1250, 50],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "// Prepare email list for summarization\nconst messages = $input.item.json.ocs?.data || [];\nconst emails = messages.map(msg => ({\n from: msg.from?.join(', ') || 'Unknown',\n subject: msg.subject || '(No Subject)',\n preview: msg.preview?.substring(0, 200) || msg.plainTextBody?.substring(0, 200) || '(No preview)',\n date: msg.dateInt ? new Date(msg.dateInt * 1000).toLocaleDateString('de-DE') : 'Unknown'\n}));\n\nconst emailList = emails.map((e, i) => `${i+1}. **${e.subject}** (von ${e.from})\\n ${e.preview}`).join('\\n\\n');\n\nreturn {\n total_emails: emails.length,\n email_list: emailList,\n emails_json: emails\n};"
},
"name": "Prepare Email Summary",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [1450, 50]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.SAIA_HOST }}/v1/chat/completions",
"authentication": "headerAuth",
"headerAuth": {
"Authorization": "Bearer {{ $env.SAIA_API_KEY }}"
},
"sendBody": true,
"bodyParametersUi": "json",
"body": "{\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"Write a concise 3-5 line daily email digest summarizing key points and action items.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"Summarize these {{ $node['Prepare Email Summary'].json.total_emails }} emails:\\n\\n{{ $node['Prepare Email Summary'].json.email_list }}\"\n }\n ],\n \"temperature\": 0.7,\n \"max_tokens\": 300\n}"
},
"name": "SAIA: Summarize Emails",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [1650, 50]
},
{
"parameters": {
"jsCode": "const summary = $input.item.json.choices?.[0]?.message?.content || 'No emails today';\nconst timestamp = new Date().toLocaleTimeString('de-DE', {timeZone: 'Europe/Berlin'});\nreturn {\n summary,\n timestamp,\n email_count: $node['Prepare Email Summary'].json.total_emails\n};"
},
"name": "Extract Summary",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [1850, 50]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.MEMOS_HOST }}/api/v1/memos",
"authentication": "headerAuth",
"headerAuth": {
"Authorization": "Bearer {{ $env.MEMOS_API_KEY }}"
},
"sendBody": true,
"bodyParametersUi": "json",
"body": "{\n \"content\": \"📧 **Email Digest** ({{ $node['Extract Summary'].json.email_count }} Mails)\\n\\n{{ $node['Extract Summary'].json.summary }}\\n\\n_Generated at {{ $node['Extract Summary'].json.timestamp }}_\",\n \"visibility\": \"PRIVATE\"\n}"
},
"name": "Post Digest to Memos",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [2050, 50]
}
],
"connections": {
"Daily Trigger (7 AM)": {
"main": [[{ "node": "Fetch Mail Accounts from Nextcloud", "type": "main", "index": 0 }]]
},
"Fetch Mail Accounts from Nextcloud": {
"main": [[{ "node": "Extract Personal Mail Account", "type": "main", "index": 0 }]]
},
"Extract Personal Mail Account": {
"main": [[{ "node": "Fetch Mailboxes", "type": "main", "index": 0 }]]
},
"Fetch Mailboxes": {
"main": [[{ "node": "Get INBOX ID", "type": "main", "index": 0 }]]
},
"Get INBOX ID": {
"main": [[{ "node": "Fetch Today's Messages", "type": "main", "index": 0 }]]
},
"Fetch Today's Messages": {
"main": [[{ "node": "Prepare Email Summary", "type": "main", "index": 0 }]]
},
"Prepare Email Summary": {
"main": [[{ "node": "SAIA: Summarize Emails", "type": "main", "index": 0 }]]
},
"SAIA: Summarize Emails": {
"main": [[{ "node": "Extract Summary", "type": "main", "index": 0 }]]
},
"Extract Summary": {
"main": [[{ "node": "Post Digest to Memos", "type": "main", "index": 0 }]]
}
}
}