1 / 14
← → navegar
F fullscreen
Parte 2 — El setup avanzado
Mi setup REAL con
Claude Code
VPS 24/7 · 4 dispositivos · Obsidian sync · Voz · Dashboard en tiempo real
El problema
Setup local = limitado
💻
Depende del PC
Apagas el ordenador y Claude se muere. Sin automatizaciones, sin bots, sin nada.
📍
Solo un dispositivo
Estás en la calle con el móvil? Mala suerte. Solo funciona en el PC donde lo instalaste.
⏳
Tareas largas
Scripts, informes, dashboards... tu PC tiene que estar encendido durante todo el proceso.
La solución: mover todo a un servidor
La solución
VPS 24/7 — tu cerebro en la nube
Configuración
De cero a operativo en 15 min
→
Paso 2
🔐
SSH
Conectar al servidor
ssh root@ip
→
Paso 3
🤖
Claude Code
Instalar con un comando
API key y listo
→
Paso 4
💻
tmux
Sesión persistente
Sobrevive desconexiones
→
Paso 5
✅
VS Code Remote
Extensión Remote SSH
Editar como si fuera local
curl -fsSL https://claude.ai/install.sh | sh
tmux new -s claude
tmux attach -t claude
Obsidian Sync
Notas sincronizadas en tiempo real
📱
Obsidian Móvil
Ideas, notas, tareas
↔
Obsidian Sync
☁️
Obsidian Cloud
Sync oficial (4$/mes)
↔
Headless Client
🤖
Claude Code
Lee y escribe notas
⚡ Cómo funciona
Claude escribe archivos directamente en la carpeta del vault. El cliente headless oficial de Obsidian detecta los cambios y los sube a Obsidian Sync. Todos tus dispositivos los reciben al instante.
🎯 Resultado
"Claudia, apunta que mañana tengo reunión con Oriol" — la nota aparece en el móvil en 2 segundos. Solución oficial, fiable, sin configuraciones complicadas.
Acceso por voz
Claudia — tu asistente por voz
🎙️
Claudia PWA
claudia.kersagency.es
- Web app progresiva instalable
- Pulsa el micro y habla
- Apunta notas, consulta datos
- Accesible desde cualquier móvil
🔊
Alexa Skill
"Alexa, abre claudia bot"
- Manos libres desde casa
- Recordatorios por voz
- Resumen del día
- Tu Jarvis personal
La estrella del setup
Dashboard en tiempo real
📈 Gráfica de ingresos · Holded API
💰 Crypto Portfolio
Glassmorphism · Dark mode · Responsive · Generado por Claude
Datos en tiempo real
Todas las APIs conectadas
💶
Holded
Facturación y contabilidad
📊
GoHighLevel
CRM, pipeline, leads
▶️
YouTube Analytics
Views, suscriptores, CTR
🌐
Google Analytics 4
Tráfico web y sesiones
📢
Google Ads
Campañas y ROAS
📱
Meta Ads
Facebook e Instagram Ads
💍
Oura Ring
Sueño, salud, recuperación
💹
Crypto / Bots
Portfolio y trading bots
Pipeline de edición
Kanban compartido con Diego
📝 Por grabar 3
Setup VPS Claude Code
Tutorial
5 automatizaciones GHL
Tutorial
GHL vs HubSpot 2026
Comparativa
🎬 Grabado 2
Marca blanca GHL
Tutorial
Review IA para agencias
Review
✂️ En edición 1
Claude Code + GHL (parte 1)
Tutorial
✅ Listo 2
Qué es GoHighLevel
Publicado
Tour comunidad KERS
Publicado
Sin WhatsApps de ida y vuelta — todo sincronizado automáticamente
Para clientes
Dashboards personalizados
Caso real
Cliente 1
Dashboard generado automáticamente con sus métricas:
- Rendimiento redes sociales
- Tráfico web y SEO
- Leads y pipeline CRM
- Comparativa mensual
Oportunidad
Valor añadido
Ofrece esto como servicio adicional a tus clientes de GHL:
- Dashboard con su branding
- Datos en tiempo real de sus APIs
- Actualización automática
- Diferenciación brutal vs competencia
Mismo sistema, misma calidad — escalable a N clientes
Lo que viene
Esto es solo el principio
💹
Trading bots automáticos
Grid bot, DCA bot, swing bot corriendo 24/7 en el servidor. Crypto en piloto automático.
🏠
CRM inmobiliario
Sistema completo para agencias inmobiliarias usando custom objects de GHL. Ya en desarrollo con Laukers.
🔗
Más integraciones
Stripe, PayPal, Notion, WhatsApp Business... todo lo que tenga API, entra en el dashboard.
🧠
Agentes autónomos
Tareas programadas que se ejecutan solas: informes diarios, limpieza de datos, alertas inteligentes.
Coste real
¿Cuánto cuesta todo esto?
Claude Pro
Suscripción mensual — el cerebro de todo
20 $/mes
Servidor 24/7 — 2 núcleos, 8 GB RAM. Enlace con descuento en la descripción
~10 $/mes
Obsidian Sync
Notas sincronizadas en todos tus dispositivos
4 $/mes
GoHighLevel
CRM + automatizaciones — 30 días gratis + comunidad
GRATIS*
Total: ~34 $/mes
*30 días gratis con el enlace de la descripción + acceso a la comunidad
Recap
Tu asistente IA completo
🖧
VPS 24/7
Asistente siempre activo, accesible desde cualquier dispositivo
📝
Notas sincronizadas
Obsidian, Notion o tu app favorita — todo conectado
🎙️
Voz
PWA en móvil + Alexa para acceso manos libres
📊
Dashboard
APIs conectadas, datos en tiempo real, personalizable
🤖
Asistente guiado
Un prompt que te configura todo paso a paso
🚀
Escalable
Dashboards para clientes, bots, CRMs personalizados
Decidme en comentarios qué tutorial queréis que haga más detallado
Tu turno
Consigue el prompt y configura tu asistente
He preparado un prompt completo que, al pegarlo en Claude Code dentro de tu VPS,
te guía paso a paso por toda la configuración: dispositivos, notas, calendario, CRM,
voz, dashboard y seguridad. Adaptado a lo que tú necesites.
1. Contrata un VPS y abre la terminal
Enlace con descuento en la descripción. Ubuntu 24.04.
2. Instalar Node.js
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
3. Instalar Claude Code
npm install -g @anthropic-ai/claude-code
4. Instalar tmux
sudo apt install -y tmux
tmux new -s claude
5. Arrancar Claude Code
claude
Autoriza con tu cuenta Anthropic (Claude Pro, 20 $/mes)
6. Pegar el prompt
Pega el prompt que recibes por email y dale a Enter.
Reconectar después
tmux attach -t claude
Consigue el prompt gratis
Introduce tu email y te lo envío al correo:
# Asistente de Configuracion -- Tu IA Personal 24/7
Soy tu asistente de configuracion. Voy a ayudarte a montar paso a paso tu propio sistema de IA personal en este servidor, adaptado a lo que necesites.
Vamos a ir pregunta por pregunta. No te preocupes si no entiendes algo -- te lo explico todo como si fuera la primera vez que tocas un servidor.
## Como funciona
1. Te hago una pregunta
2. Tu respondes
3. Yo configuro lo que haga falta
4. Probamos que funciona
5. Siguiente pregunta
No saltamos a la siguiente hasta que la actual este funcionando. Si algo falla, no te preocupes. Dime que error ves y lo arreglamos juntos.
**Tiempo total estimado: entre 1.5 y 3 horas** (depende de cuantas integraciones quieras).
---
## Sistema de checkpoints
Cada vez que completemos una fase, guardo el progreso en `~/.assistant-config.json`. Si se desconecta la sesion o quieres continuar otro dia, solo tienes que decirme "continuar setup" y retomo donde lo dejamos.
```bash
# Si necesitas ver en que punto estas:
cat ~/.assistant-config.json
```
---
## FASE 1: Informacion basica (5 min)
### Pregunta 1.1
**Como te llamas?**
(Lo uso para personalizar todo -- el asistente, las notas, los dashboards)
> [Espero tu respuesta. Cuando respondas, guardo tu nombre en el config.]
```python
# Lo que hago internamente cuando respondas:
import json
config = {"name": "", "business": "", "use_case": "", "completed_phases": [], "devices": [], "integrations": {}}
config["name"] = "TU_NOMBRE"
with open(os.path.expanduser("~/.assistant-config.json"), "w") as f:
json.dump(config, f, indent=2)
```
### Pregunta 1.2
**A que te dedicas?**
Esto me ayuda a configurar las integraciones que mas te sirvan. Ejemplos:
- Agencia de marketing
- Freelance (diseno, desarrollo, consultoria...)
- Ecommerce
- Inmobiliaria
- Creador de contenido
- Otro (dime cual)
> [Espero tu respuesta]
### Pregunta 1.3
**Cual va a ser el uso principal de tu asistente?**
- **A) Productividad personal** -- notas, calendario, recordatorios, organizacion
- **B) Negocio** -- CRM, leads, facturacion, dashboards, automatizaciones
- **C) Ambos** (recomendado)
> [Segun tu respuesta, ajusto que fases incluir. Si dices A, omito CRM y dashboard de negocio. Si dices B, omito notas personales. Si dices C, hacemos todo.]
### Pregunta 1.4
**Tienes ya un dominio apuntando a este servidor?**
Un dominio es la direccion web (como "tuempresa.com"). Lo necesitamos para:
- Que puedas acceder al dashboard desde cualquier sitio
- Que la app de voz funcione en el movil (necesita HTTPS)
- Que puedas conectar Alexa
Si no tienes dominio, puedo ayudarte a conseguir uno (desde 1 euro/mes) o podemos usar la IP directamente para empezar (con limitaciones).
Opciones:
- **Si, tengo dominio**: dimelo (ej: miweb.com)
- **Si, pero no esta apuntando al servidor**: te ayudo a configurar los DNS
- **No tengo dominio**: seguimos sin el por ahora, ya lo anadimos luego
> [Espero tu respuesta]
### Checkpoint Fase 1
```bash
# Guardo tu configuracion base
cat > ~/.assistant-config.json << 'CHECKPOINT'
{
"name": "TU_NOMBRE",
"business": "TU_NEGOCIO",
"use_case": "personal|negocio|ambos",
"domain": "tudominio.com o null",
"completed_phases": ["fase1"],
"devices": [],
"integrations": {},
"credentials_dir": "~/assistant/credentials"
}
CHECKPOINT
```
**Fase 1 completada.** Vamos con los dispositivos.
---
## FASE 2: Dispositivos -- acceder desde cualquier sitio (10-15 min por dispositivo)
> [Si quieres configurar esto mas adelante, dime "saltar" y pasamos a la siguiente fase.]
### Pregunta 2.1
**Desde que dispositivos quieres acceder a tu asistente?**
Marca todos los que apliquen:
- **PC/Laptop Windows**
- **PC/Laptop Mac**
- **PC/Laptop Linux**
- **Android** (movil o tablet)
- **iPhone/iPad**
- **Alexa** (Amazon Echo, etc.)
> [Espero tu respuesta. Configuro cada uno en orden.]
---
### 2A. tmux -- que tu asistente no se apague nunca
Antes de conectar dispositivos, necesitamos que Claude Code siga corriendo aunque cierres todo. Para eso usamos tmux.
**Que es tmux?** Piensa en ello como dejar una ventana abierta en el servidor. Cuando cierras tu portatil, la ventana sigue ahi. Cuando vuelves, la abres y todo sigue como lo dejaste.
```bash
# Instalar tmux
sudo apt update && sudo apt install tmux -y
# Crear una sesion que sobrevive a la desconexion
tmux new -s claude
# Dentro de tmux, arranca Claude Code
claude
# Para "desengancharte" (dejar corriendo en segundo plano):
# Pulsa Ctrl+B, luego suelta, y pulsa D
# Para volver a conectarte cuando quieras:
tmux attach -t claude
```
**TEST:** Prueba ahora:
1. Dentro de tmux, escribe `echo "funciona"`
2. Pulsa Ctrl+B, luego D (te desconectas)
3. Escribe `tmux attach -t claude` (vuelves y sigue ahi)
Funciona? Perfecto. Si no, dime que ves y lo arreglamos.
---
### 2B. PC/Laptop Windows
#### Paso 1: Instalar VS Code
1. Ve a https://code.visualstudio.com/
2. Haz clic en el boton azul grande que dice "Download for Windows"
3. Ejecuta el instalador. Dale a "Siguiente" a todo (deja las opciones por defecto)
4. Cuando termine, abre VS Code
#### Paso 2: Instalar la extension Remote-SSH
1. En VS Code, haz clic en el icono de cuadraditos a la izquierda (Extensions) o pulsa Ctrl+Shift+X
2. En la barra de busqueda, escribe: `Remote - SSH`
3. La primera que aparece es de Microsoft. Haz clic en "Install"
4. Espera a que termine
#### Paso 3: Generar tu clave SSH
Abre PowerShell (busca "PowerShell" en el menu de inicio) y escribe:
```powershell
# Generar clave SSH (dale a Enter a todo, no pongas contrasena)
ssh-keygen -t ed25519 -C "tu-email@ejemplo.com"
# Ver tu clave publica (la necesitamos para el servidor)
cat $env:USERPROFILE\.ssh\id_ed25519.pub
```
Copia el texto que aparece (empieza por `ssh-ed25519`). Mandamelo.
#### Paso 4: Anadir tu clave al servidor
Yo ejecuto esto en el servidor:
```bash
# Crear el directorio si no existe
mkdir -p ~/.ssh
# Anadir tu clave
echo "AQUI_VA_TU_CLAVE_PUBLICA" >> ~/.ssh/authorized_keys
# Permisos correctos
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys
```
#### Paso 5: Configurar la conexion en VS Code
En tu PC Windows, abre PowerShell y escribe:
```powershell
# Crear el archivo de configuracion SSH
notepad $env:USERPROFILE\.ssh\config
```
Pega este contenido (cambiando los datos):
```
Host mi-servidor
HostName IP_DEL_SERVIDOR
User root
IdentityFile ~/.ssh/id_ed25519
Port 22
ServerAliveInterval 60
ServerAliveCountMax 3
```
Guarda y cierra.
#### Paso 6: Conectarte
1. En VS Code, pulsa Ctrl+Shift+P
2. Escribe: "Remote-SSH: Connect to Host"
3. Selecciona "mi-servidor"
4. Espera a que se conecte (la primera vez tarda un poco)
5. Cuando pregunte por el sistema operativo, selecciona "Linux"
#### Paso 7: Instalar Claude Code extension
1. Ya conectado al servidor en VS Code, ve a Extensions (Ctrl+Shift+X)
2. Busca "Claude Code" de Anthropic
3. Haz clic en "Install in SSH: mi-servidor"
#### TEST
1. Abre un terminal en VS Code (Ctrl+`)
2. Escribe `claude`
3. Si ves el prompt de Claude Code, todo funciona
**Funciona? Si ves algun error, pegamelo aqui.**
---
### 2C. PC/Laptop Mac
#### Paso 1: Instalar VS Code
1. Ve a https://code.visualstudio.com/
2. Descarga la version para Mac (Apple Silicon o Intel segun tu Mac)
3. Arrastra VS Code a la carpeta Aplicaciones
4. Abrelo
#### Paso 2: Instalar Remote-SSH
Igual que en Windows:
1. Extensions (Cmd+Shift+X)
2. Busca "Remote - SSH"
3. Install
#### Paso 3: Generar clave SSH
Abre Terminal (esta en Aplicaciones > Utilidades > Terminal) y escribe:
```bash
# Generar clave SSH
ssh-keygen -t ed25519 -C "tu-email@ejemplo.com"
# Dale a Enter a todo, no pongas contrasena
# Ver tu clave publica
cat ~/.ssh/id_ed25519.pub
```
Copia el texto que aparece y mandamelo.
#### Paso 4: Anadir clave al servidor
Igual que en Windows -- yo lo hago en el servidor.
#### Paso 5: Configurar conexion
```bash
# Editar configuracion SSH
nano ~/.ssh/config
```
Pega:
```
Host mi-servidor
HostName IP_DEL_SERVIDOR
User root
IdentityFile ~/.ssh/id_ed25519
Port 22
ServerAliveInterval 60
ServerAliveCountMax 3
```
Guarda con Ctrl+O, Enter, Ctrl+X.
#### Paso 6-7 y TEST
Igual que en Windows.
---
### 2D. PC/Laptop Linux
Exactamente igual que Mac. La Terminal ya esta instalada. Los comandos son los mismos.
---
### 2E. Android
El movil lo configuramos en la **Fase 6 (Acceso por voz)**. Ahi montamos una app web (PWA) que funciona como una app nativa en tu Android -- la instalas desde el navegador y aparece en tu pantalla de inicio como cualquier otra app.
Si ademas quieres acceso SSH desde Android (para usar Claude Code directamente, como en el PC):
1. Instala la app **Termux** desde F-Droid (no Google Play, esa version esta desactualizada)
- Ve a https://f-droid.org/packages/com.termux/
- Descarga e instala
2. Abre Termux y escribe:
```bash
pkg update && pkg install openssh -y
ssh-keygen -t ed25519
cat ~/.ssh/id_ed25519.pub
```
3. Mandame la clave publica y la anado al servidor
4. Para conectarte: `ssh root@IP_DEL_SERVIDOR`
---
### 2F. iPhone/iPad
Igual que Android, el acceso principal sera la PWA en la Fase 6.
Para SSH desde iPhone/iPad:
1. Instala la app **Blink Shell** (de pago, pero la mejor) o **a-Shell** (gratis)
2. Genera clave SSH dentro de la app
3. Configuracion similar a Mac
---
### 2G. Alexa
Lo configuramos en la **Fase 6**. Necesitamos primero el servidor web con HTTPS.
---
### Checkpoint Fase 2
```json
{
"completed_phases": ["fase1", "fase2"],
"devices": ["windows", "mac", "android"],
"tmux_configured": true,
"ssh_keys_added": true
}
```
**Fase 2 completada.** Ahora vamos con las notas.
---
## FASE 3: App de notas -- tu segundo cerebro (15-20 min)
### Pregunta 3.1
**Que app de notas quieres usar?**
Tu asistente podra leer y escribir notas, crear recordatorios, guardar ideas... pero necesita conectarse a donde guardes tus notas.
- **A) Obsidian** (recomendado -- es gratis, tus datos son tuyos, sincroniza con todos tus dispositivos)
- **B) Notion** (popular, buena para equipos, tiene API oficial)
- **C) Google Keep** (no tiene API oficial -- no lo recomiendo para esto)
- **D) Apple Notes** (muy limitado desde fuera del ecosistema Apple)
- **E) Archivos simples** (sin app, solo archivos de texto en el servidor)
- **F) No quiero notas** (saltamos esta fase)
> [Espero tu respuesta]
---
### 3A. Obsidian (recomendado)
Te explico el plan: Obsidian guarda las notas como archivos de texto en tu PC/movil. Para que Claude pueda leer y escribir esas notas desde el servidor, usamos **Obsidian Sync** (4$/mes) con su **cliente headless** oficial. Asi, cuando Claude escribe una nota, aparece automaticamente en tu Obsidian del movil y viceversa. Es la solucion oficial de Obsidian, fiable y sin complicaciones.
#### Paso 1: Crear cuenta de Obsidian y contratar Sync
1. Ve a https://obsidian.md/account y crea una cuenta (o usa la que ya tengas)
2. Contrata **Obsidian Sync Standard** (4$/mes) desde https://obsidian.md/pricing
3. **Apunta tu email y contrasena** -- los necesitamos para el servidor
> [Espero tu confirmacion. Si quieres configurar Obsidian mas adelante, dime "saltar" y seguimos con la siguiente fase.]
#### Paso 2: Instalar Obsidian en tus dispositivos
- **PC/Mac/Linux:** Ve a https://obsidian.md/ y descarga
- **Android:** Busca "Obsidian" en Google Play Store
- **iPhone:** Busca "Obsidian" en App Store
Abre Obsidian y crea un vault nuevo (o usa uno existente).
#### Paso 3: Activar Sync en un dispositivo
1. Abre Obsidian en tu PC o Mac
2. Ve a **Settings > Core plugins** y activa **Sync**
3. Haz clic en el icono de Sync (flechitas) que aparece en la barra lateral
4. Inicia sesion con tu cuenta de Obsidian
5. Crea un **vault remoto** (ponle un nombre, ej: "Mi Vault")
6. Elige un password de encriptacion y **apuntalo** (lo necesitamos para el servidor)
7. Espera a que se sincronice el vault
Repite la activacion de Sync en todos tus dispositivos (movil, otro PC...) conectandote al mismo vault remoto.
#### Paso 4: Instalar el cliente headless en el servidor
```bash
# Requisito: Node.js 22+
node --version # Debe ser v22 o superior
# Instalar obsidian-headless
sudo npm install -g obsidian-headless
```
#### Paso 5: Conectar el servidor al vault
```bash
# Login con tu cuenta de Obsidian
ob login --email TU_EMAIL
# Ver los vaults disponibles
ob sync-list-remote
# Conectar al vault (cambia los valores)
ob sync-setup \
--vault "NOMBRE_DE_TU_VAULT" \
--path ~/obsidian-vault \
--password "TU_PASSWORD_DE_ENCRIPTACION" \
--device-name "VPS-Claude"
```
Te preguntara el estado del vault. Selecciona la opcion que corresponda:
- Si el vault local esta vacio: **"This Vault is empty"**
- Si ya tiene archivos: **"There may be differences"**
#### Paso 6: Crear el servicio de sincronizacion continua
```bash
# Crear servicio systemd
sudo cat > /etc/systemd/system/obsidian-sync.service << 'EOF'
[Unit]
Description=Obsidian Headless Sync
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/ob sync --path /root/obsidian-vault --continuous
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
# Activar y arrancar
sudo systemctl daemon-reload
sudo systemctl enable obsidian-sync
sudo systemctl start obsidian-sync
# Verificar que funciona
sudo systemctl status obsidian-sync
```
Debe aparecer "active (running)" y en los logs "Fully synced".
#### Paso 7: Crear el script para que Claude escriba notas
```bash
mkdir -p ~/assistant/scripts
cat > ~/assistant/scripts/obsidian-write.py << 'SCRIPT'
#!/usr/bin/env python3
"""
Escribir notas en Obsidian. El servicio headless las sincroniza automaticamente.
Uso: python3 obsidian-write.py "Carpeta/Mi nota.md" "Contenido de la nota"
echo "contenido" | python3 obsidian-write.py "Carpeta/Mi nota.md"
"""
import os, sys
VAULT_PATH = os.path.expanduser("~/obsidian-vault")
def write_note(note_path, content):
full_path = os.path.join(VAULT_PATH, note_path)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, "w", encoding="utf-8") as f:
f.write(content)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Uso: obsidian-write.py <ruta> [contenido]")
sys.exit(1)
note_path = sys.argv[1]
content = sys.argv[2] if len(sys.argv) >= 3 else sys.stdin.read()
write_note(note_path, content)
print(f"OK: {note_path}")
SCRIPT
chmod +x ~/assistant/scripts/obsidian-write.py
```
**Nota:** Ajusta `VAULT_PATH` si tu vault esta en otra ruta.
#### TEST: Escribir una nota de prueba
```bash
python3 ~/assistant/scripts/obsidian-write.py "Test/nota-de-prueba.md" "Hola! Si ves esto en Obsidian, la sincronizacion funciona."
```
Espera unos segundos y abre Obsidian en tu movil o PC. Deberia aparecer una carpeta "Test" con el archivo "nota-de-prueba.md".
**Aparece? Si no, dime que error ves.**
#### Paso 8: Configurar Claude para que lea notas
Claude lee notas directamente del directorio del vault:
```bash
# Claude puede leer cualquier nota asi:
cat ~/obsidian-vault/Carpeta/Mi\ nota.md
# O buscar en todas las notas:
grep -r "texto a buscar" ~/obsidian-vault/
```
Como el sync es bidireccional, las notas que tu crees en el movil o PC tambien aparecen en el servidor automaticamente.
---
### 3B. Notion
#### Paso 1: Crear una integracion en Notion
1. Ve a https://www.notion.so/my-integrations
2. Haz clic en **+ New integration**
3. Ponle un nombre (ej: "Mi Asistente IA")
4. Selecciona el workspace donde quieres que funcione
5. Haz clic en **Submit**
6. Copia el **Internal Integration Token** (empieza por `ntn_` o `secret_`)
7. **Mandame el token** (lo guardo de forma segura en el servidor)
#### Paso 2: Compartir tus paginas con la integracion
En Notion, las integraciones solo acceden a las paginas que TU les compartas:
1. Abre la pagina o base de datos que quieras que Claude pueda leer/escribir
2. Haz clic en **"..."** (arriba a la derecha)
3. Haz clic en **"Connections"** o **"Add connections"**
4. Busca tu integracion y anadela
5. Repite para cada pagina que quieras conectar
#### Paso 3: Instalar la libreria y crear helper
```bash
pip install notion-client
# Guardar token
mkdir -p ~/assistant/credentials
echo "TU_TOKEN_AQUI" > ~/assistant/credentials/notion_token.txt
chmod 600 ~/assistant/credentials/notion_token.txt
# Crear helper
cat > ~/assistant/scripts/notion_helper.py << 'SCRIPT'
#!/usr/bin/env python3
"""Helper para leer/escribir en Notion."""
from notion_client import Client
import json
TOKEN_FILE = "$HOME/assistant/credentials/notion_token.txt"
token = open(TOKEN_FILE.replace("$HOME", __import__("os").path.expanduser("~"))).read().strip()
notion = Client(auth=token)
def search_pages(query=""):
"""Buscar paginas en Notion."""
results = notion.search(query=query, filter={"property": "object", "value": "page"})
return [{"id": p["id"], "title": p.get("properties", {}).get("title", {}).get("title", [{}])[0].get("text", {}).get("content", "Sin titulo")} for p in results.get("results", [])]
def read_page(page_id):
"""Leer el contenido de una pagina."""
blocks = notion.blocks.children.list(block_id=page_id)
text = ""
for block in blocks.get("results", []):
btype = block["type"]
if btype in ("paragraph", "heading_1", "heading_2", "heading_3", "bulleted_list_item", "numbered_list_item"):
rich_text = block[btype].get("rich_text", [])
text += "".join(rt.get("text", {}).get("content", "") for rt in rich_text) + "\n"
return text
def add_to_page(page_id, content):
"""Anadir un bloque de texto a una pagina."""
notion.blocks.children.append(
block_id=page_id,
children=[{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [{"type": "text", "text": {"content": content}}]
}
}]
)
return True
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "search":
query = sys.argv[2] if len(sys.argv) > 2 else ""
for page in search_pages(query):
print(f"{page['id']}: {page['title']}")
elif len(sys.argv) > 2 and sys.argv[1] == "read":
print(read_page(sys.argv[2]))
elif len(sys.argv) > 3 and sys.argv[1] == "write":
add_to_page(sys.argv[2], sys.argv[3])
print("OK")
SCRIPT
```
#### TEST
```bash
# Buscar paginas compartidas
python3 ~/assistant/scripts/notion_helper.py search
# Deberia mostrar las paginas que compartiste con la integracion
```
**Ves tus paginas? Si no aparece nada, asegurate de que compartiste la pagina con la integracion (Paso 2).**
---
### 3C. Google Keep
Google Keep no tiene API oficial. Esto significa que no hay una forma fiable y estable de conectarlo. Opciones:
1. **Cambiar a Obsidian** (recomendado) -- tus datos son tuyos, sincroniza gratis, Claude tiene acceso completo
2. **Cambiar a Notion** -- tiene API oficial, funciona bien
3. **Seguir con Keep pero sin integracion** -- Claude no podra leer ni escribir en Keep. Puedes usar archivos de texto en el servidor como alternativa
Que prefieres?
---
### 3D. Apple Notes
Apple Notes solo funciona dentro del ecosistema Apple. Desde un servidor Linux, las opciones son muy limitadas:
1. **Siri Shortcuts** -- puedes crear un atajo que envia notas al servidor. Pero solo funciona de Apple -> servidor, no al reves
2. **Cambiar a Obsidian o Notion** para las notas que interactuen con Claude
Si quieres seguir con Apple Notes para uso personal y usar Obsidian/archivos para lo que toque Claude, podemos hacer eso.
---
### 3E. Archivos simples
La opcion mas sencilla. Claude lee y escribe archivos de texto directamente en el servidor. Sin sincronizacion a otros dispositivos (a menos que montes Syncthing o algo asi).
```bash
# Crear estructura de carpetas
mkdir -p ~/notes/{personal,trabajo,ideas,recordatorios}
# Claude puede leer y escribir directamente aqui
echo "Mi primera nota" > ~/notes/ideas/primera-idea.md
```
Ventaja: sencillo, sin configurar nada.
Desventaja: no puedes ver las notas desde el movil (a menos que accedas por SSH).
---
### Checkpoint Fase 3
```json
{
"completed_phases": ["fase1", "fase2", "fase3"],
"integrations": {
"notes": "obsidian|notion|files|none"
}
}
```
**Fase 3 completada.** Vamos con el calendario.
---
## FASE 4: Calendario -- que Claude sepa que tienes hoy (10 min)
> [Si quieres configurar esto mas adelante, dime "saltar" y pasamos a la siguiente fase.]
### Pregunta 4.1
**Que calendario usas?**
- **A) Google Calendar** (recomendado -- API muy buena)
- **B) Outlook / Microsoft 365**
- **C) Apple Calendar (iCal)**
- **D) No uso calendario / no quiero conectarlo**
> [Espero tu respuesta]
---
### 4A. Google Calendar
Necesitamos crear un "proyecto" en Google Cloud para que Claude pueda leer tu calendario. Suena complicado, pero son solo clics.
#### Paso 1: Crear un proyecto en Google Cloud
1. Ve a https://console.cloud.google.com/
2. Si es tu primera vez, acepta los terminos
3. Arriba a la izquierda, haz clic en el nombre del proyecto (o "Select a project")
4. Haz clic en **"NEW PROJECT"**
5. Nombre: "Mi Asistente IA" (o lo que quieras)
6. Haz clic en **"CREATE"**
7. Espera a que se cree y asegurate de que esta seleccionado
#### Paso 2: Activar la API de Calendar
1. Ve a https://console.cloud.google.com/apis/library/calendar-json.googleapis.com
2. Asegurate de que el proyecto correcto esta seleccionado arriba
3. Haz clic en **"ENABLE"**
#### Paso 3: Crear credenciales OAuth
1. Ve a https://console.cloud.google.com/apis/credentials
2. Haz clic en **"+ CREATE CREDENTIALS"** > **"OAuth client ID"**
3. Si te pide configurar la pantalla de consentimiento:
- User Type: **External** (luego "CREATE")
- App name: "Mi Asistente"
- User support email: tu email
- Developer email: tu email
- Haz clic en "SAVE AND CONTINUE" hasta el final
- En "Test users", anade tu email de Google
- Haz clic en "SAVE AND CONTINUE"
- Vuelve a Credentials
4. Ahora si: **"+ CREATE CREDENTIALS"** > **"OAuth client ID"**
5. Application type: **"Desktop app"**
6. Nombre: "Asistente IA"
7. Haz clic en **"CREATE"**
8. Haz clic en **"DOWNLOAD JSON"**
9. Mandame el contenido del archivo (o subelo al servidor)
#### Paso 4: Guardar credenciales y autorizar
```bash
mkdir -p ~/assistant/credentials
# Pegar el contenido del JSON que descargaste:
cat > ~/assistant/credentials/google_client.json << 'CREDS'
PEGA_AQUI_EL_CONTENIDO_DEL_JSON
CREDS
# Instalar la libreria
pip install google-auth-oauthlib google-auth-httplib2 google-api-python-client
# Ejecutar el flujo de autorizacion
python3 << 'AUTH'
from google_auth_oauthlib.flow import InstalledAppFlow
import json
SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"]
flow = InstalledAppFlow.from_client_secrets_file(
"$HOME/assistant/credentials/google_client.json".replace("$HOME", __import__("os").path.expanduser("~")),
SCOPES
)
# Esto abre una URL -- copiala en tu navegador
creds = flow.run_console()
token_data = {
"refresh_token": creds.refresh_token,
"token": creds.token,
"token_uri": creds.token_uri,
"client_id": creds.client_id,
"client_secret": creds.client_secret,
}
with open("$HOME/assistant/credentials/google_token.json".replace("$HOME", __import__("os").path.expanduser("~")), "w") as f:
json.dump(token_data, f, indent=2)
print("Token guardado correctamente!")
AUTH
```
Te aparecera una URL. Copiala en tu navegador, inicia sesion con tu cuenta de Google, acepta los permisos, y pega el codigo que te da.
#### Paso 5: Crear helper para calendario
```bash
cat > ~/assistant/scripts/google_helpers.py << 'SCRIPT'
#!/usr/bin/env python3
"""Helper para obtener un access token fresco de Google."""
import json, requests, os
HOME = os.path.expanduser("~")
CREDENTIALS_FILE = f"{HOME}/assistant/credentials/google_client.json"
TOKEN_FILE = f"{HOME}/assistant/credentials/google_token.json"
def get_access_token():
try:
with open(TOKEN_FILE) as f:
token = json.load(f)
with open(CREDENTIALS_FILE) as f:
client = json.load(f)["installed"]
r = requests.post("https://oauth2.googleapis.com/token", data={
"client_id": client["client_id"],
"client_secret": client["client_secret"],
"refresh_token": token["refresh_token"],
"grant_type": "refresh_token",
})
data = r.json()
if "access_token" not in data:
return None
return data["access_token"]
except Exception as e:
print(f"Error: {e}")
return None
def headers():
token = get_access_token()
if token is None:
return {"Authorization": "Bearer invalid"}
return {"Authorization": f"Bearer {token}"}
SCRIPT
```
#### TEST
```bash
python3 << 'TEST'
import sys
sys.path.insert(0, "$HOME/assistant/scripts".replace("$HOME", __import__("os").path.expanduser("~")))
from google_helpers import headers
import requests
from datetime import datetime, timedelta
h = headers()
now = datetime.utcnow().isoformat() + "Z"
later = (datetime.utcnow() + timedelta(days=7)).isoformat() + "Z"
r = requests.get(
f"https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin={now}&timeMax={later}&singleEvents=true&orderBy=startTime",
headers=h
)
events = r.json().get("items", [])
if events:
print(f"Tienes {len(events)} eventos en los proximos 7 dias:")
for e in events[:5]:
start = e["start"].get("dateTime", e["start"].get("date", ""))
print(f" - {e.get('summary', 'Sin titulo')}: {start}")
else:
print("No tienes eventos en los proximos 7 dias (o el calendario esta vacio)")
TEST
```
**Ves tus eventos? Si dice "invalid", revisa que el token se guardo bien.**
---
### 4B. Outlook / Microsoft 365
#### Paso 1: Registrar app en Azure
1. Ve a https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade
2. Haz clic en **"New registration"**
3. Nombre: "Mi Asistente IA"
4. Supported account types: "Accounts in any organizational directory and personal Microsoft accounts"
5. Redirect URI: "Web" > `http://localhost:8080`
6. Haz clic en **"Register"**
7. Copia el **Application (client) ID**
#### Paso 2: Crear un secreto
1. En la app que acabas de crear, ve a **"Certificates & secrets"**
2. **"New client secret"**
3. Descripcion: "asistente"
4. Expiracion: 24 meses
5. Copia el **Value** (NO el Secret ID)
#### Paso 3: Permisos
1. Ve a **"API permissions"**
2. **"Add a permission"** > **"Microsoft Graph"** > **"Delegated permissions"**
3. Busca y anade: `Calendars.Read`
4. Haz clic en **"Grant admin consent"** (si aparece)
#### Paso 4: Configurar en el servidor
```bash
pip install msal requests
mkdir -p ~/assistant/credentials
cat > ~/assistant/credentials/outlook_config.json << 'CONFIG'
{
"client_id": "TU_CLIENT_ID",
"client_secret": "TU_CLIENT_SECRET",
"authority": "https://login.microsoftonline.com/common",
"scope": ["https://graph.microsoft.com/Calendars.Read"]
}
CONFIG
# Crear helper
cat > ~/assistant/scripts/outlook_helper.py << 'SCRIPT'
#!/usr/bin/env python3
"""Helper para Microsoft Graph API (Outlook Calendar)."""
import json, os, msal, requests
HOME = os.path.expanduser("~")
CONFIG_FILE = f"{HOME}/assistant/credentials/outlook_config.json"
TOKEN_CACHE = f"{HOME}/assistant/credentials/outlook_token_cache.json"
with open(CONFIG_FILE) as f:
config = json.load(f)
cache = msal.SerializableTokenCache()
if os.path.exists(TOKEN_CACHE):
cache.deserialize(open(TOKEN_CACHE).read())
app = msal.ConfidentialClientApplication(
config["client_id"],
authority=config["authority"],
client_credential=config["client_secret"],
token_cache=cache
)
def get_token():
accounts = app.get_accounts()
if accounts:
result = app.acquire_token_silent(config["scope"], account=accounts[0])
if result and "access_token" in result:
return result["access_token"]
# Need interactive auth
flow = app.initiate_device_flow(config["scope"])
print(flow["message"])
result = app.acquire_token_by_device_flow(flow)
with open(TOKEN_CACHE, "w") as f:
f.write(cache.serialize())
return result.get("access_token")
def get_events(days=7):
from datetime import datetime, timedelta
token = get_token()
headers = {"Authorization": f"Bearer {token}"}
now = datetime.utcnow().isoformat() + "Z"
end = (datetime.utcnow() + timedelta(days=days)).isoformat() + "Z"
r = requests.get(
f"https://graph.microsoft.com/v1.0/me/calendarView?startDateTime={now}&endDateTime={end}&$orderby=start/dateTime",
headers=headers
)
return r.json().get("value", [])
if __name__ == "__main__":
events = get_events()
for e in events:
print(f"- {e.get('subject', 'Sin titulo')}: {e['start']['dateTime']}")
SCRIPT
```
La primera vez te pedira que vayas a una URL y metas un codigo. Despues de eso, funciona automaticamente.
---
### 4C. Apple Calendar
Apple Calendar usa el protocolo CalDAV. La integracion es limitada comparada con Google/Outlook.
**Recomendacion:** Si usas Apple Calendar, la forma mas facil es anadir tu Google Calendar en la app de Calendario de Apple (funcionan juntos). Asi Claude accede via Google API y tu sigues usando la app de Apple.
Si insistes en CalDAV directo:
```bash
pip install caldav
cat > ~/assistant/scripts/caldav_helper.py << 'SCRIPT'
#!/usr/bin/env python3
"""Helper para CalDAV (Apple Calendar, etc.)."""
import caldav
from datetime import datetime, timedelta
# Para iCloud:
# URL: https://caldav.icloud.com
# Usuario: tu Apple ID
# Password: una App-Specific Password (generala en https://appleid.apple.com)
URL = "https://caldav.icloud.com"
USERNAME = "tu-apple-id@icloud.com"
PASSWORD = "tu-app-specific-password"
client = caldav.DAVClient(url=URL, username=USERNAME, password=PASSWORD)
principal = client.principal()
calendars = principal.calendars()
for cal in calendars:
print(f"Calendario: {cal.name}")
events = cal.date_search(datetime.now(), datetime.now() + timedelta(days=7))
for e in events:
print(f" - {e.vobject_instance.vevent.summary.value}")
SCRIPT
```
---
### Checkpoint Fase 4
```json
{
"completed_phases": ["fase1", "fase2", "fase3", "fase4"],
"integrations": {
"notes": "obsidian",
"calendar": "google|outlook|apple|none"
}
}
```
**Fase 4 completada.** Ahora el CRM.
---
## FASE 5: CRM -- gestiona tus contactos y clientes (10 min)
> [Si quieres configurar esto mas adelante, dime "saltar" y pasamos a la siguiente fase.]
### Pregunta 5.1
**Usas algun CRM?**
- **A) GoHighLevel** (automatizacion, funnels, CRM todo-en-uno)
- **B) HubSpot** (CRM gratuito popular)
- **C) Salesforce** (enterprise)
- **D) Otro** (dimelo)
- **E) No uso CRM**
> [Espero tu respuesta]
---
### 5A. GoHighLevel
#### Paso 1: Obtener tu API Key
1. Entra en GoHighLevel (app.gohighlevel.com o tu white-label)
2. Ve a **Settings** > **Business Profile**
3. Busca **API Key** (o ve a Settings > Company > API Keys si eres agencia)
4. Copia la API Key
5. Tambien necesitas el **Location ID**: ve a Settings > Business Profile > mira la URL, el ID esta ahi
#### Paso 2: Guardar credenciales
```bash
mkdir -p ~/assistant/credentials
# Guardar token (pega tu API key)
echo "TU_API_KEY" > ~/assistant/credentials/ghl_token.txt
chmod 600 ~/assistant/credentials/ghl_token.txt
# Guardar Location ID
echo "TU_LOCATION_ID" > ~/assistant/credentials/ghl_location.txt
chmod 600 ~/assistant/credentials/ghl_location.txt
```
#### Paso 3: Crear helpers
```bash
cat > ~/assistant/scripts/ghl_helper.py << 'SCRIPT'
#!/usr/bin/env python3
"""Helper para GoHighLevel API."""
import requests, json, os
HOME = os.path.expanduser("~")
CREDS = f"{HOME}/assistant/credentials"
TOKEN = open(f"{CREDS}/ghl_token.txt").read().strip()
LOCATION = open(f"{CREDS}/ghl_location.txt").read().strip()
BASE = "https://services.leadconnectorhq.com"
def headers():
return {"Authorization": f"Bearer {TOKEN}", "Version": "2021-07-28", "Content-Type": "application/json"}
def list_contacts(limit=20):
r = requests.get(f"{BASE}/contacts/?locationId={LOCATION}&limit={limit}", headers=headers())
return r.json().get("contacts", [])
def search_contact(query):
r = requests.get(f"{BASE}/contacts/search?locationId={LOCATION}&query={query}", headers=headers())
return r.json().get("contacts", [])
def create_contact(name, email=None, phone=None, tags=None):
parts = name.split(" ", 1)
data = {"locationId": LOCATION, "firstName": parts[0]}
if len(parts) > 1:
data["lastName"] = parts[1]
if email: data["email"] = email
if phone: data["phone"] = phone
if tags: data["tags"] = tags
r = requests.post(f"{BASE}/contacts/", headers=headers(), json=data)
return r.json()
def list_pipelines():
r = requests.get(f"{BASE}/opportunities/pipelines?locationId={LOCATION}", headers=headers())
return r.json().get("pipelines", [])
def list_opportunities(pipeline_id):
r = requests.get(f"{BASE}/opportunities/search?location_id={LOCATION}&pipeline_id={pipeline_id}", headers=headers())
return r.json().get("opportunities", [])
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
if sys.argv[1] == "contacts":
for c in list_contacts():
name = f"{c.get('firstName','')} {c.get('lastName','')}".strip()
print(f" {name} | {c.get('email','')} | {c.get('phone','')}")
elif sys.argv[1] == "pipelines":
for p in list_pipelines():
print(f" {p['name']} (ID: {p['id']})")
else:
print("Uso: ghl_helper.py [contacts|pipelines]")
SCRIPT
```
#### TEST
```bash
python3 ~/assistant/scripts/ghl_helper.py contacts
```
**Ves tus contactos? Si sale un error 401, revisa que la API Key este bien copiada.**
---
### 5B. HubSpot
#### Paso 1: Crear una app privada
1. Ve a https://app.hubspot.com/
2. Settings (engranaje) > Integrations > Private Apps
3. **"Create a private app"**
4. Nombre: "Mi Asistente IA"
5. En Scopes, activa:
- `crm.objects.contacts.read`
- `crm.objects.contacts.write`
- `crm.objects.deals.read`
6. **"Create app"** > Copia el token
#### Paso 2: Configurar
```bash
pip install hubspot-api-client
echo "TU_TOKEN" > ~/assistant/credentials/hubspot_token.txt
chmod 600 ~/assistant/credentials/hubspot_token.txt
cat > ~/assistant/scripts/hubspot_helper.py << 'SCRIPT'
#!/usr/bin/env python3
"""Helper para HubSpot API."""
from hubspot import HubSpot
import os
HOME = os.path.expanduser("~")
TOKEN = open(f"{HOME}/assistant/credentials/hubspot_token.txt").read().strip()
client = HubSpot(access_token=TOKEN)
def list_contacts(limit=10):
response = client.crm.contacts.basic_api.get_page(limit=limit)
return [{"id": c.id, "name": f"{c.properties.get('firstname','')} {c.properties.get('lastname','')}".strip(), "email": c.properties.get("email","")} for c in response.results]
def list_deals(limit=10):
response = client.crm.deals.basic_api.get_page(limit=limit)
return [{"id": d.id, "name": d.properties.get("dealname",""), "amount": d.properties.get("amount",""), "stage": d.properties.get("dealstage","")} for d in response.results]
if __name__ == "__main__":
print("Contactos:")
for c in list_contacts():
print(f" {c['name']} | {c['email']}")
print("\nDeals:")
for d in list_deals():
print(f" {d['name']} | {d['amount']}EUR | Stage: {d['stage']}")
SCRIPT
```
#### TEST
```bash
python3 ~/assistant/scripts/hubspot_helper.py
```
---
### 5E. No uso CRM
Sin problema. Si mas adelante quieres probar uno, GoHighLevel tiene 14 dias gratis. Solo dime y lo configuramos.
---
### Checkpoint Fase 5
```json
{
"completed_phases": ["fase1", "fase2", "fase3", "fase4", "fase5"],
"integrations": {
"notes": "obsidian",
"calendar": "google",
"crm": "ghl|hubspot|none"
}
}
```
**Fase 5 completada.** Ahora la parte mas chula: acceso por voz.
---
## FASE 6: Acceso por voz -- habla con tu asistente desde el movil o Alexa (20-30 min)
> [Si quieres configurar esto mas adelante, dime "saltar" y pasamos a la siguiente fase.]
Esta fase tiene 3 partes:
1. Montar el servidor web (backend)
2. Crear la app de voz (PWA para movil)
3. Conectar Alexa (opcional)
**Requisito:** Necesitas un dominio con HTTPS para que funcione en moviles. Si no configuraste uno en la Fase 1, lo hacemos ahora.
### 6.0: Configurar dominio y HTTPS (si no lo tienes)
#### Opcion A: Tienes dominio
```bash
# Instalar nginx y certbot
sudo apt install -y nginx certbot python3-certbot-nginx
# Crear configuracion de nginx
sudo cat > /etc/nginx/sites-available/asistente << 'NGINX'
server {
listen 80;
server_name TU_DOMINIO.COM;
location /api/ {
proxy_pass http://127.0.0.1:8082;
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;
}
location /app/ {
proxy_pass http://127.0.0.1:8082/api/voice/;
proxy_set_header Host $host;
}
}
NGINX
# Activar el sitio
sudo ln -sf /etc/nginx/sites-available/asistente /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
# Obtener certificado SSL (HTTPS)
sudo certbot --nginx -d TU_DOMINIO.COM
# Te pide tu email y aceptar terminos. Cuando pregunte si redirigir HTTP a HTTPS, di que si.
```
#### Opcion B: No tienes dominio
Puedes usar la IP directamente, pero el microfono del navegador no funciona sin HTTPS (excepto en localhost). Alternativas:
1. Comprar un dominio barato (Namecheap desde 1 EUR)
2. Usar un subdominio gratuito con DuckDNS (https://www.duckdns.org/)
Para DuckDNS:
1. Ve a https://www.duckdns.org/ y logueate con Google
2. Crea un subdominio (ej: "mi-asistente.duckdns.org")
3. Pon la IP de tu servidor
4. Luego usa ese subdominio en la configuracion de nginx y certbot
---
### 6.1: Crear el servidor de voz (backend)
```bash
# Instalar dependencias
pip install flask anthropic requests
# Crear directorio
mkdir -p ~/assistant/scripts
mkdir -p ~/assistant/pwa
# Necesitas una API key de Anthropic (Claude)
# Ve a https://console.anthropic.com/ > API Keys > Create Key
echo "TU_API_KEY_DE_ANTHROPIC" > ~/assistant/credentials/claude_api_key.txt
chmod 600 ~/assistant/credentials/claude_api_key.txt
# Generar un token de seguridad para la app
python3 -c "import secrets; print(secrets.token_urlsafe(32))" > ~/assistant/credentials/app_token.txt
chmod 600 ~/assistant/credentials/app_token.txt
echo "Token generado. Guardalo, lo necesitaras para la app:"
cat ~/assistant/credentials/app_token.txt
```
Ahora el servidor Flask:
```bash
cat > ~/assistant/scripts/voice_server.py << 'SCRIPT'
#!/usr/bin/env python3
"""
Servidor de voz -- API endpoint + PWA para hablar con tu asistente IA.
Puerto: 8082, proxied via nginx en /api/
"""
from flask import Flask, request, jsonify, send_from_directory
import anthropic
import json, os, subprocess
from datetime import datetime
from pathlib import Path
app = Flask(__name__)
# ── Config ──
HOME = os.path.expanduser("~")
CREDS = f"{HOME}/assistant/credentials"
API_KEY = open(f"{CREDS}/claude_api_key.txt").read().strip()
APP_TOKEN = open(f"{CREDS}/app_token.txt").read().strip()
PWA_DIR = f"{HOME}/assistant/pwa"
# Si tienes Obsidian configurado:
OBSIDIAN_SCRIPT = f"{HOME}/assistant/scripts/obsidian-write.py"
HAS_OBSIDIAN = os.path.exists(OBSIDIAN_SCRIPT)
# Si tienes calendario configurado:
CALENDAR_HELPER = f"{HOME}/assistant/scripts/google_helpers.py"
HAS_CALENDAR = os.path.exists(CALENDAR_HELPER)
client = anthropic.Anthropic(api_key=API_KEY)
# Leer config para personalizar
config = {}
config_file = f"{HOME}/.assistant-config.json"
if os.path.exists(config_file):
config = json.load(open(config_file))
USER_NAME = config.get("name", "usuario")
SYSTEM_PROMPT = f"""Eres un asistente personal de {USER_NAME}.
Respondes en espanol, tono informal y directo, como un amigo.
Sin emojis nunca. Respuestas MUY cortas (1-2 frases maximo) porque se leen en voz alta.
NUNCA hagas preguntas de vuelta. Actua directamente.
Fecha actual: {{date}}
REGLAS:
- Si pide apuntar/recordar/anotar algo: responde SOLO con el JSON {{"action": "note", "path": "Notas/voz.md", "content": "lo que dijo"}} y NADA mas.
- Si pregunta algo: responde directamente en 1-2 frases.
- NUNCA preguntes "quieres que apunte" o "necesitas algo mas".
"""
def write_obsidian_note(path, content):
if not HAS_OBSIDIAN:
return False
try:
result = subprocess.run(
["python3", OBSIDIAN_SCRIPT, path, content],
capture_output=True, text=True, timeout=10
)
return result.returncode == 0
except:
return False
@app.route("/api/chat", methods=["POST"])
def chat():
token = request.headers.get("X-App-Token", "")
if token != APP_TOKEN:
return jsonify({"error": "No autorizado"}), 403
data = request.get_json(force=True, silent=True) or {}
user_msg = data.get("message", "").strip()
if not user_msg:
return jsonify({"error": "Mensaje vacio"}), 400
today = datetime.now().strftime("%Y-%m-%d %H:%M")
system = SYSTEM_PROMPT.replace("{date}", today)
try:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=300,
system=system,
messages=[{"role": "user", "content": user_msg}]
)
reply = response.content[0].text
# Detectar si quiere guardar una nota
import re
note_written = False
if '{"action": "note"' in reply or '{"action":"note"' in reply:
try:
json_match = re.search(r'\{[^}]*"action"\s*:\s*"note"[^}]*\}', reply)
if json_match:
note_data = json.loads(json_match.group())
path = note_data.get("path", "Notas/voz.md")
content = note_data.get("content", user_msg)
timestamped = f"\n\n## {today}\n{content}"
note_written = write_obsidian_note(path, timestamped)
reply = "Apuntado." if note_written else "No he podido guardar la nota."
except:
pass
return jsonify({"reply": reply, "note_written": note_written, "timestamp": today})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/health", methods=["GET"])
def health():
return jsonify({"status": "ok"})
# Servir la PWA
@app.route("/api/voice/<path:filename>")
def serve_pwa(filename):
return send_from_directory(PWA_DIR, filename)
@app.route("/api/voice/")
def serve_pwa_index():
return send_from_directory(PWA_DIR, "index.html")
# ── Alexa Skill endpoint ──
def alexa_response(speech, should_end=True):
resp = {
"version": "1.0",
"response": {
"outputSpeech": {"type": "SSML", "ssml": f"<speak>{speech}</speak>"},
"shouldEndSession": should_end
}
}
if not should_end:
resp["response"]["reprompt"] = {
"outputSpeech": {"type": "SSML", "ssml": "<speak>Dime.</speak>"}
}
return jsonify(resp)
@app.route("/api/alexa", methods=["POST"])
def alexa_skill():
data = request.get_json(force=True, silent=True) or {}
req_type = data.get("request", {}).get("type", "")
if req_type == "LaunchRequest":
return alexa_response(f"Hola {USER_NAME}, dime que necesitas.", should_end=False)
if req_type == "SessionEndedRequest":
return alexa_response("Hasta luego.", should_end=True)
if req_type == "IntentRequest":
intent = data.get("request", {}).get("intent", {})
intent_name = intent.get("name", "")
slots = intent.get("slots", {})
user_msg = slots.get("query", {}).get("value", "")
if intent_name in ("AMAZON.StopIntent", "AMAZON.CancelIntent"):
return alexa_response("Hasta luego.")
if intent_name == "AMAZON.HelpIntent":
return alexa_response("Puedes preguntarme lo que necesites.", should_end=False)
if intent_name == "AMAZON.FallbackIntent":
return alexa_response("No te he pillado. Dimelo de otra forma.", should_end=False)
if not user_msg:
return alexa_response("No te he entendido. Repitemelo.", should_end=False)
today = datetime.now().strftime("%Y-%m-%d %H:%M")
system = SYSTEM_PROMPT.replace("{date}", today)
try:
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=150,
system=system,
messages=[{"role": "user", "content": user_msg}]
)
reply = response.content[0].text
import re
if '{"action": "note"' in reply or '{"action":"note"' in reply:
try:
json_match = re.search(r'\{[^}]*"action"\s*:\s*"note"[^}]*\}', reply)
if json_match:
note_data = json.loads(json_match.group())
path = note_data.get("path", "Notas/voz.md")
content = note_data.get("content", user_msg)
timestamped = f"\n\n## {today}\n{content}"
if write_obsidian_note(path, timestamped):
reply = "Apuntado."
else:
reply = "No he podido guardar la nota."
except:
pass
return alexa_response(reply, should_end=False)
except:
return alexa_response("Ha habido un error. Intentalo de nuevo.", should_end=False)
return alexa_response("No te he entendido.", should_end=False)
if __name__ == "__main__":
os.makedirs(PWA_DIR, exist_ok=True)
app.run(host="127.0.0.1", port=8082, debug=False)
SCRIPT
```
---
### 6.2: Crear la app de voz (PWA)
La PWA (Progressive Web App) es una pagina web que se comporta como una app nativa. La instalas desde el navegador en tu movil y aparece como un icono mas en tu pantalla de inicio.
```bash
# Crear el manifest (dice al navegador que es una "app instalable")
cat > ~/assistant/pwa/manifest.json << 'MANIFEST'
{
"name": "Asistente IA",
"short_name": "Asistente",
"description": "Tu asistente personal de IA",
"start_url": "/app/",
"scope": "/app/",
"display": "standalone",
"background_color": "#080810",
"theme_color": "#080810",
"orientation": "portrait",
"icons": [
{"src": "icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable"},
{"src": "icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable"}
]
}
MANIFEST
# Service Worker (para que funcione offline)
cat > ~/assistant/pwa/sw.js << 'SW'
const CACHE_NAME = 'asistente-v1';
const ASSETS = ['/app/', '/app/index.html', '/app/manifest.json'];
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)));
self.skipWaiting();
});
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))))
);
self.clients.claim();
});
self.addEventListener('fetch', (e) => {
if (e.request.url.includes('/chat') || e.request.url.includes('/api/')) return;
e.respondWith(caches.match(e.request).then((r) => r || fetch(e.request)));
});
SW
```
Ahora la pagina HTML principal. Esta es la interfaz de chat con boton de microfono:
```bash
cat > ~/assistant/pwa/index.html << 'HTML'
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#080810">
<meta name="mobile-web-app-capable" content="yes">
<link rel="manifest" href="manifest.json">
<title>Asistente IA</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #080810; color: #F5F5F7;
min-height: 100vh; min-height: 100dvh;
display: flex; flex-direction: column; overflow: hidden;
}
body::before {
content: ''; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background:
radial-gradient(ellipse at 20% 0%, rgba(12,171,174,0.12) 0%, transparent 50%),
radial-gradient(ellipse at 80% 100%, rgba(155,143,212,0.10) 0%, transparent 50%);
pointer-events: none; z-index: 0;
}
body > * { position: relative; z-index: 1; }
.header {
display: flex; align-items: center; justify-content: center;
padding: 16px 20px 10px;
background: rgba(12,12,20,0.6);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.header h1 {
font-size: 20px; font-weight: 600;
background: linear-gradient(135deg, #0CABAE, #9B8FD4);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.chat-area {
flex: 1; overflow-y: auto; padding: 12px 20px;
display: flex; flex-direction: column; gap: 10px;
-webkit-overflow-scrolling: touch;
}
.msg {
max-width: 85%; padding: 12px 16px; border-radius: 18px;
font-size: 15px; line-height: 1.45; word-wrap: break-word;
animation: fadeIn 0.3s ease;
}
.msg.user {
align-self: flex-end;
background: linear-gradient(135deg, rgba(12,171,174,0.85), rgba(10,138,140,0.85));
color: white; border-bottom-right-radius: 6px;
}
.msg.assistant {
align-self: flex-start;
background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,255,255,0.08);
color: #E0E0E8; border-bottom-left-radius: 6px;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
.input-area {
display: flex; gap: 8px; padding: 12px 16px;
background: rgba(12,12,20,0.8);
backdrop-filter: blur(20px);
border-top: 1px solid rgba(255,255,255,0.06);
padding-bottom: max(12px, env(safe-area-inset-bottom));
}
.input-area input {
flex: 1; padding: 12px 16px; border-radius: 24px;
background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.1);
color: #F5F5F7; font-size: 15px; outline: none;
}
.input-area input::placeholder { color: rgba(255,255,255,0.3); }
.input-area input:focus { border-color: rgba(12,171,174,0.5); }
.btn {
width: 48px; height: 48px; border-radius: 50%; border: none;
display: flex; align-items: center; justify-content: center; cursor: pointer;
transition: all 0.2s;
}
.btn-mic {
background: linear-gradient(135deg, #0CABAE, #0A8A8C);
}
.btn-mic.recording {
background: linear-gradient(135deg, #e74c3c, #c0392b);
animation: pulse 1s infinite;
}
.btn-send {
background: rgba(255,255,255,0.1);
}
.btn svg { width: 22px; height: 22px; fill: white; }
@keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.1); } }
.status {
text-align: center; padding: 4px; font-size: 12px; color: rgba(255,255,255,0.3);
}
</style>
</head>
<body>
<div class="header">
<h1>Asistente IA</h1>
</div>
<div class="chat-area" id="chatArea">
<div class="msg assistant">Hola! Escribe o pulsa el microfono para hablar.</div>
</div>
<div class="status" id="status"></div>
<div class="input-area">
<input type="text" id="textInput" placeholder="Escribe aqui..." autocomplete="off">
<button class="btn btn-mic" id="micBtn" onclick="toggleMic()">
<svg viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5-3c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>
</button>
<button class="btn btn-send" onclick="sendText()">
<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</button>
</div>
<script>
// ── CONFIGURACION ──
// CAMBIA ESTO: pon tu token de la app (el que se genero en app_token.txt)
const APP_TOKEN = "TU_APP_TOKEN_AQUI";
// CAMBIA ESTO: pon tu dominio o IP
const API_URL = "/api/chat";
const chatArea = document.getElementById('chatArea');
const textInput = document.getElementById('textInput');
const micBtn = document.getElementById('micBtn');
const status = document.getElementById('status');
let recognition = null;
let isRecording = false;
// Reconocimiento de voz
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
recognition = new SpeechRecognition();
recognition.lang = 'es-ES';
recognition.continuous = false;
recognition.interimResults = true;
recognition.onresult = (e) => {
const transcript = Array.from(e.results).map(r => r[0].transcript).join('');
textInput.value = transcript;
if (e.results[0].isFinal) {
stopMic();
sendMessage(transcript);
}
};
recognition.onerror = (e) => {
stopMic();
status.textContent = 'Error de microfono: ' + e.error;
};
recognition.onend = () => { stopMic(); };
} else {
micBtn.style.display = 'none';
}
function toggleMic() {
if (isRecording) { stopMic(); } else { startMic(); }
}
function startMic() {
if (!recognition) return;
isRecording = true;
micBtn.classList.add('recording');
status.textContent = 'Escuchando...';
recognition.start();
}
function stopMic() {
isRecording = false;
micBtn.classList.remove('recording');
status.textContent = '';
try { recognition.stop(); } catch(e) {}
}
function sendText() {
const text = textInput.value.trim();
if (text) sendMessage(text);
}
textInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') sendText();
});
function addMessage(text, role) {
const div = document.createElement('div');
div.className = `msg ${role}`;
div.textContent = text;
chatArea.appendChild(div);
chatArea.scrollTop = chatArea.scrollHeight;
}
async function sendMessage(text) {
addMessage(text, 'user');
textInput.value = '';
status.textContent = 'Pensando...';
try {
const r = await fetch(API_URL, {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-App-Token': APP_TOKEN},
body: JSON.stringify({message: text})
});
const data = await r.json();
status.textContent = '';
if (data.error) {
addMessage('Error: ' + data.error, 'assistant');
return;
}
addMessage(data.reply, 'assistant');
// Leer en voz alta
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(data.reply);
utterance.lang = 'es-ES';
utterance.rate = 1.1;
speechSynthesis.speak(utterance);
}
} catch(e) {
status.textContent = '';
addMessage('Error de conexion. Intentalo de nuevo.', 'assistant');
}
}
// Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js');
}
</script>
</body>
</html>
HTML
```
**IMPORTANTE:** Edita `index.html` y pon tu APP_TOKEN (el que se genero antes).
#### Crear iconos para la app
```bash
# Generar iconos basicos (cuadrados de color con las iniciales)
python3 << 'ICONS'
# Crear iconos PNG simples
import struct, zlib
def create_icon(size, filename):
"""Crear un icono PNG simple (cuadrado de color con gradiente)."""
pixels = []
for y in range(size):
row = []
for x in range(size):
# Gradiente de verde-azul a morado
r = int(12 + (x/size) * 140)
g = int(171 - (x/size) * 30)
b = int(174 + (x/size) * 40)
row.extend([r, g, b, 255])
pixels.append(bytes([0] + row)) # filter byte + pixel data
raw = b''.join(pixels)
def make_chunk(chunk_type, data):
chunk = chunk_type + data
return struct.pack('>I', len(data)) + chunk + struct.pack('>I', zlib.crc32(chunk) & 0xffffffff)
ihdr = struct.pack('>IIBBBBB', size, size, 8, 6, 0, 0, 0)
png = b'\x89PNG\r\n\x1a\n'
png += make_chunk(b'IHDR', ihdr)
png += make_chunk(b'IDAT', zlib.compress(raw))
png += make_chunk(b'IEND', b'')
with open(filename, 'wb') as f:
f.write(png)
import os
HOME = os.path.expanduser("~")
create_icon(192, f"{HOME}/assistant/pwa/icon-192.png")
create_icon(512, f"{HOME}/assistant/pwa/icon-512.png")
print("Iconos creados")
ICONS
```
---
### 6.3: Arrancar el servidor y probarlo
```bash
# Crear un servicio systemd para que arranque automaticamente
sudo cat > /etc/systemd/system/voice-assistant.service << 'SERVICE'
[Unit]
Description=Voice Assistant Server
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/assistant/scripts
ExecStart=/usr/bin/python3 /root/assistant/scripts/voice_server.py
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
SERVICE
# Activar y arrancar
sudo systemctl daemon-reload
sudo systemctl enable voice-assistant
sudo systemctl start voice-assistant
```
#### TEST: Probar el servidor
```bash
# Comprobar que esta corriendo
curl http://localhost:8082/api/health
# Debe responder: {"status":"ok"}
# Probar enviar un mensaje
curl -X POST http://localhost:8082/api/chat \
-H "Content-Type: application/json" \
-H "X-App-Token: $(cat ~/assistant/credentials/app_token.txt)" \
-d '{"message": "hola, como estas?"}'
# Debe responder con un JSON con "reply"
```
#### TEST: Probar desde el movil
1. Abre el navegador en tu movil
2. Ve a `https://TU_DOMINIO.COM/app/` (o `http://IP:8082/api/voice/` si estas en la red local)
3. Deberia verse la interfaz de chat
4. Pulsa el boton de microfono y di algo
5. Deberia responder en texto y en voz alta
#### Instalar como app en Android:
1. En Chrome, abre la pagina
2. Pulsa los 3 puntos arriba a la derecha
3. "Instalar app" o "Agregar a pantalla de inicio"
4. Ahora aparece como una app mas
#### Instalar como app en iPhone:
1. En Safari, abre la pagina
2. Pulsa el icono de compartir (cuadrado con flecha arriba)
3. "Agregar a pantalla de inicio"
4. Ponle nombre y "Agregar"
---
### 6.4: Conectar Alexa (opcional)
Para que Alexa funcione, necesitas crear una "Skill" en la consola de Amazon.
#### Paso 1: Crear la Skill
1. Ve a https://developer.amazon.com/alexa/console/ask
2. Si no tienes cuenta de desarrollador, creala (es gratis)
3. Haz clic en **"Create Skill"**
4. Nombre: "Mi Asistente" (o lo que quieras)
5. Locale: **"Spanish (ES)"** o **"Spanish (MX)"**
6. Model: **"Custom"**
7. Backend: **"Provision your own"**
8. Haz clic en **"Create skill"**
9. Template: **"Start from Scratch"**
#### Paso 2: Configurar el Intent
1. En el menu de la izquierda, haz clic en **"Interaction Model"** > **"Intents"**
2. Haz clic en **"+ Add Intent"**
3. Nombre del intent: `AskAssistantIntent`
4. Haz clic en **"Create custom intent"**
5. Anade un slot:
- Nombre: `query`
- Tipo: `AMAZON.SearchQuery`
6. Sample utterances (escribelas una por una y dale a Enter):
```
{query}
pregunta {query}
dime {query}
apunta {query}
recuerda {query}
que {query}
necesito {query}
```
7. Haz clic en **"Save Model"**
8. Haz clic en **"Build Model"** (espera a que termine)
#### Paso 3: Configurar el Endpoint
1. En el menu de la izquierda, haz clic en **"Endpoint"**
2. Selecciona **"HTTPS"**
3. Default Region: `https://TU_DOMINIO.COM/api/alexa`
4. SSL Certificate Type: selecciona **"My development endpoint has a certificate from a trusted certificate authority"** (si usaste certbot)
5. Haz clic en **"Save Endpoints"**
#### Paso 4: Probar
1. Ve a la pestana **"Test"** (arriba)
2. Activa "Skill testing is enabled in: Development"
3. Escribe o di: "abre mi asistente" (o el nombre de invocacion que le pusiste)
4. Luego di lo que quieras
#### Paso 5: Usar en tu Echo real
La skill en modo "development" ya funciona en tu Echo si esta vinculado a la misma cuenta de Amazon del desarrollador. Solo di "Alexa, abre mi asistente".
---
### Checkpoint Fase 6
```json
{
"completed_phases": ["fase1", "fase2", "fase3", "fase4", "fase5", "fase6"],
"integrations": {
"notes": "obsidian",
"calendar": "google",
"crm": "ghl",
"voice": {
"pwa": true,
"alexa": true,
"domain": "tudominio.com"
}
}
}
```
**Fase 6 completada.** Ahora los dashboards.
---
## FASE 7: Dashboard -- tus datos de un vistazo (30-45 min)
> [Si quieres configurar esto mas adelante, dime "saltar" y pasamos a la siguiente fase.]
### Pregunta 7.1
**Que datos quieres ver en tu dashboard?**
Marca todos los que apliquen:
- **Facturacion** (Holded, Stripe, facturas Excel)
- **YouTube** (visitas, suscriptores, mejores videos)
- **Google Analytics** (trafico web)
- **Redes sociales** (Instagram, Facebook, TikTok)
- **CRM** (nuevos leads, oportunidades, pipeline)
- **Salud** (Oura Ring, Apple Health)
- **Inversiones** (criptomonedas, acciones)
- **Ads** (Google Ads, Meta Ads)
> [Segun tu respuesta, configuro cada fuente de datos]
---
### 7.1: Facturacion
#### Con Holded:
```bash
# Obtener tu API key de Holded:
# 1. Ve a https://app.holded.com/
# 2. Settings > API > Generate API Key
echo "TU_HOLDED_API_KEY" > ~/assistant/credentials/holded_key.txt
chmod 600 ~/assistant/credentials/holded_key.txt
```
#### Con Stripe:
```bash
# 1. Ve a https://dashboard.stripe.com/apikeys
# 2. Copia la clave secreta (empieza por sk_live_)
echo "TU_STRIPE_KEY" > ~/assistant/credentials/stripe_key.txt
chmod 600 ~/assistant/credentials/stripe_key.txt
```
#### Con Excel/manual:
```bash
mkdir -p ~/assistant/data
# Sube tu archivo Excel al servidor y lo parseamos con openpyxl
pip install openpyxl
```
---
### 7.2: YouTube Analytics
Usa la misma configuracion de Google Cloud de la Fase 4. Solo necesitas activar una API mas:
1. Ve a https://console.cloud.google.com/apis/library/youtubeanalytics.googleapis.com
2. Haz clic en **"ENABLE"**
3. Tambien activa: https://console.cloud.google.com/apis/library/youtube.googleapis.com
Luego re-autoriza anadiendo los scopes:
```bash
python3 << 'AUTH'
from google_auth_oauthlib.flow import InstalledAppFlow
import json, os
HOME = os.path.expanduser("~")
SCOPES = [
"https://www.googleapis.com/auth/calendar.readonly",
"https://www.googleapis.com/auth/yt-analytics.readonly",
"https://www.googleapis.com/auth/youtube.readonly"
]
flow = InstalledAppFlow.from_client_secrets_file(f"{HOME}/assistant/credentials/google_client.json", SCOPES)
creds = flow.run_console()
token_data = {
"refresh_token": creds.refresh_token,
"token": creds.token,
"token_uri": creds.token_uri,
"client_id": creds.client_id,
"client_secret": creds.client_secret,
}
with open(f"{HOME}/assistant/credentials/google_token.json", "w") as f:
json.dump(token_data, f, indent=2)
print("Token actualizado con permisos de YouTube!")
AUTH
```
---
### 7.3: Google Analytics (GA4)
1. Activa la API: https://console.cloud.google.com/apis/library/analyticsdata.googleapis.com
2. Re-autoriza anadiendo el scope `https://www.googleapis.com/auth/analytics.readonly`
3. Necesitas tu **Property ID** de GA4:
- Ve a https://analytics.google.com/
- Admin > Property Settings > Property ID (numero)
```bash
echo "TU_GA4_PROPERTY_ID" > ~/assistant/credentials/ga4_property_id.txt
```
---
### 7.4: Redes sociales (Meta / Instagram)
1. Ve a https://developers.facebook.com/
2. Crea una app (tipo "Business")
3. Anade el producto "Facebook Login"
4. Obtener Page Access Token largo:
- Graph API Explorer: https://developers.facebook.com/tools/explorer/
- Selecciona tu app
- Anade permisos: `pages_show_list`, `pages_read_engagement`, `instagram_basic`, `instagram_manage_insights`
- Genera token
- Extiende a token de larga duracion:
```bash
curl "https://graph.facebook.com/v18.0/oauth/access_token?grant_type=fb_exchange_token&client_id=TU_APP_ID&client_secret=TU_APP_SECRET&fb_exchange_token=TU_TOKEN_CORTO"
```
5. Guarda el token:
```bash
echo "TU_TOKEN_LARGO" > ~/assistant/credentials/meta_token.txt
chmod 600 ~/assistant/credentials/meta_token.txt
```
---
### 7.5: Salud (Oura Ring)
```bash
# 1. Ve a https://cloud.ouraring.com/personal-access-tokens
# 2. Crea un nuevo token
echo "TU_OURA_TOKEN" > ~/assistant/credentials/oura_token.txt
chmod 600 ~/assistant/credentials/oura_token.txt
# Test
curl -H "Authorization: Bearer $(cat ~/assistant/credentials/oura_token.txt)" \
"https://api.ouraring.com/v2/usercollection/daily_sleep?start_date=$(date -d 'yesterday' +%Y-%m-%d)"
```
---
### 7.6: Generar el dashboard
Genero un dashboard HTML personalizado con las fuentes de datos que hayas configurado. El script Python lo genera y lo sirve via nginx.
```bash
mkdir -p ~/assistant/dashboard
# Crear script generador (simplificado -- se personaliza segun tus datos)
cat > ~/assistant/scripts/generate_dashboard.py << 'SCRIPT'
#!/usr/bin/env python3
"""
Generador de dashboard personalizado.
Ejecutar: python3 generate_dashboard.py
Resultado: ~/assistant/dashboard/index.html
"""
import json, os, sys, requests
from datetime import datetime, timedelta
HOME = os.path.expanduser("~")
sys.path.insert(0, f"{HOME}/assistant/scripts")
OUTPUT = f"{HOME}/assistant/dashboard/index.html"
CREDS = f"{HOME}/assistant/credentials"
CONFIG = json.load(open(f"{HOME}/.assistant-config.json")) if os.path.exists(f"{HOME}/.assistant-config.json") else {}
USER_NAME = CONFIG.get("name", "Dashboard")
# ── Recopilar datos ──
sections_html = ""
cards_data = []
# Facturacion (Holded)
holded_key_file = f"{CREDS}/holded_key.txt"
if os.path.exists(holded_key_file):
try:
key = open(holded_key_file).read().strip()
r = requests.get("https://api.holded.com/api/invoicing/v1/documents/invoice?starttmp=" +
str(int((datetime.now().replace(day=1, hour=0, minute=0, second=0)).timestamp())),
headers={"key": key}, timeout=10)
invoices = r.json()
total = sum(float(inv.get("total", 0)) for inv in invoices)
cards_data.append(("Facturacion mes", f"{total:,.0f} EUR", len(invoices), "facturas"))
except Exception as e:
print(f" [WARN] Holded: {e}")
# CRM (GHL)
ghl_token_file = f"{CREDS}/ghl_token.txt"
ghl_location_file = f"{CREDS}/ghl_location.txt"
if os.path.exists(ghl_token_file) and os.path.exists(ghl_location_file):
try:
token = open(ghl_token_file).read().strip()
location = open(ghl_location_file).read().strip()
r = requests.get(
f"https://services.leadconnectorhq.com/contacts/?locationId={location}&limit=50",
headers={"Authorization": f"Bearer {token}", "Version": "2021-07-28"}, timeout=10
)
contacts = r.json().get("contacts", [])
cutoff = datetime.utcnow() - timedelta(days=30)
recent = [c for c in contacts if c.get("dateAdded", "") > cutoff.isoformat()]
cards_data.append(("Leads (30 dias)", str(len(recent)), len(contacts), "total contactos"))
except Exception as e:
print(f" [WARN] GHL: {e}")
# YouTube
try:
from google_helpers import headers as google_headers
h = google_headers()
r = requests.get(
"https://youtubeanalytics.googleapis.com/v2/reports?ids=channel==MINE"
f"&startDate={(datetime.now() - timedelta(days=28)).strftime('%Y-%m-%d')}"
f"&endDate={datetime.now().strftime('%Y-%m-%d')}"
"&metrics=views,subscribersGained&dimensions=day",
headers=h, timeout=10
)
if r.status_code == 200:
yt_data = r.json()
rows = yt_data.get("rows", [])
total_views = sum(row[1] for row in rows) if rows else 0
total_subs = sum(row[2] for row in rows) if rows else 0
cards_data.append(("YouTube (28d)", f"{total_views:,} views", total_subs, "nuevos subs"))
except:
pass
# ── Generar HTML ──
cards_html = ""
for title, value, sub_value, sub_label in cards_data:
cards_html += f"""
<div class="card">
<div class="card-title">{title}</div>
<div class="card-value">{value}</div>
<div class="card-sub">{sub_value} {sub_label}</div>
</div>"""
html = f"""<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - {USER_NAME}</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: #0a0a0f; color: #f5f5f7; padding: 20px;
}}
h1 {{
text-align: center; font-size: 28px; margin-bottom: 24px;
background: linear-gradient(135deg, #0CABAE, #9B8FD4);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}}
.timestamp {{
text-align: center; color: rgba(255,255,255,0.3); font-size: 12px; margin-bottom: 24px;
}}
.grid {{
display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px; max-width: 1200px; margin: 0 auto;
}}
.card {{
background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px; padding: 20px;
}}
.card-title {{ color: rgba(255,255,255,0.5); font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; }}
.card-value {{ font-size: 32px; font-weight: 700; margin: 8px 0; }}
.card-sub {{ color: rgba(255,255,255,0.4); font-size: 14px; }}
</style>
</head>
<body>
<h1>Dashboard {USER_NAME}</h1>
<div class="timestamp">Actualizado: {datetime.now().strftime('%d/%m/%Y %H:%M')}</div>
<div class="grid">{cards_html}</div>
</body>
</html>"""
os.makedirs(os.path.dirname(OUTPUT), exist_ok=True)
with open(OUTPUT, "w") as f:
f.write(html)
print(f"Dashboard generado: {OUTPUT}")
SCRIPT
# Ejecutar para generar el primer dashboard
python3 ~/assistant/scripts/generate_dashboard.py
```
#### Servir el dashboard via nginx (si tienes dominio):
```bash
# Anadir al archivo de nginx existente:
sudo nano /etc/nginx/sites-available/asistente
```
Anade dentro del bloque `server`:
```nginx
location /dashboard/ {
alias /root/assistant/dashboard/;
index index.html;
auth_basic "Dashboard";
auth_basic_user_file /etc/nginx/.htpasswd;
}
```
Proteger con contrasena:
```bash
sudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd TU_USUARIO
# Te pide contrasena -- ponla
sudo nginx -t && sudo systemctl reload nginx
```
#### Actualizar automaticamente (cada hora):
```bash
(crontab -l 2>/dev/null; echo "0 * * * * python3 $HOME/assistant/scripts/generate_dashboard.py") | crontab -
```
#### TEST
Abre `https://TU_DOMINIO.COM/dashboard/` en el navegador. Te pide usuario y contrasena, y deberia mostrar el dashboard.
---
### Checkpoint Fase 7
```json
{
"completed_phases": ["fase1", "fase2", "fase3", "fase4", "fase5", "fase6", "fase7"],
"integrations": {
"notes": "obsidian",
"calendar": "google",
"crm": "ghl",
"voice": {"pwa": true, "alexa": true},
"dashboard": {
"sources": ["holded", "youtube", "ghl"],
"url": "https://tudominio.com/dashboard/"
}
}
}
```
**Fase 7 completada.** Ahora aseguramos el servidor.
---
## FASE 8: Seguridad -- que nadie entre donde no debe (10 min)
> [Puedes saltar esta fase, pero es recomendado no saltar -- la seguridad del servidor es importante.]
### 8.1: Cambiar el puerto SSH
Por defecto, SSH escucha en el puerto 22. Todos los bots del mundo intentan entrar por ahi. Cambiarlo reduce los ataques automaticos un 99%.
```bash
# Elegir un puerto entre 1024 y 65535 (ej: 2222, 4422, 8822...)
NUEVO_PUERTO=2222
# Editar configuracion de SSH
sudo nano /etc/ssh/sshd_config
# Busca la linea "Port 22" (puede estar comentada con #)
# Cambiala a:
# Port 2222
# Reiniciar SSH (NO CIERRES ESTA SESION hasta verificar que funciona)
sudo systemctl restart sshd
# IMPORTANTE: Abre una NUEVA terminal y prueba conectarte con el nuevo puerto:
# ssh -p 2222 root@TU_IP
# Solo cuando funcione, cierra la sesion antigua
```
**AVISO:** Si cambias el puerto y cierras la sesion sin verificar, puedes quedarte fuera del servidor. Siempre prueba PRIMERO con otra terminal.
Tambien actualiza el archivo `~/.ssh/config` de tu PC:
```
Host mi-servidor
Port 2222 # <-- cambiar aqui
```
### 8.2: Instalar fail2ban
fail2ban bloquea automaticamente las IPs que intentan entrar con contrasenas incorrectas.
```bash
sudo apt install -y fail2ban
# Configurar
sudo cat > /etc/fail2ban/jail.local << 'JAIL'
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
JAIL
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
# Ver IPs baneadas
sudo fail2ban-client status sshd
```
### 8.3: Firewall (ufw)
```bash
# Instalar y configurar firewall
sudo apt install -y ufw
# Permitir tu puerto SSH
sudo ufw allow 2222/tcp
# Permitir HTTP y HTTPS (para nginx)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Activar el firewall
sudo ufw enable
# Confirma con "y"
# Ver estado
sudo ufw status
```
### 8.4: Backup automatico
```bash
cat > ~/assistant/scripts/backup.sh << 'BACKUP'
#!/bin/bash
# Backup diario de credenciales y datos
DATE=$(date +%Y-%m-%d)
BACKUP_DIR="$HOME/backups/$DATE"
mkdir -p "$BACKUP_DIR"
# Copiar credenciales
cp -r ~/assistant/credentials "$BACKUP_DIR/"
# Copiar datos
cp -r ~/assistant/data "$BACKUP_DIR/" 2>/dev/null
# Copiar configuracion
cp ~/.assistant-config.json "$BACKUP_DIR/" 2>/dev/null
# Backup de notas de Obsidian
cp -r ~/obsidian-vault "$BACKUP_DIR/obsidian-vault" 2>/dev/null
# Mantener solo los ultimos 7 dias
find ~/backups -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \;
echo "Backup completado: $BACKUP_DIR"
BACKUP
chmod +x ~/assistant/scripts/backup.sh
# Programar a las 3 de la manana
(crontab -l 2>/dev/null; echo "0 3 * * * $HOME/assistant/scripts/backup.sh") | crontab -
```
---
### Checkpoint Fase 8
```json
{
"completed_phases": ["fase1", "fase2", "fase3", "fase4", "fase5", "fase6", "fase7", "fase8"],
"security": {
"ssh_port": 2222,
"fail2ban": true,
"ufw": true,
"backups": "daily_3am"
}
}
```
**Fase 8 completada.** Ultimo paso: la verificacion final.
---
## FASE 9: Test final -- verificar que todo funciona (5 min)
Vamos a probar todo lo que hemos configurado. Te digo que probar para cada cosa:
### Checklist
```
[ ] TMUX: tmux attach -t claude --> ves Claude Code corriendo
[ ] SSH: Conectarte desde tu PC con VS Code
[ ] NOTAS: Escribir una nota de test y verla en el movil
python3 ~/assistant/scripts/obsidian-write.py "Test/verificacion.md" "Todo funciona!"
[ ] CALENDARIO: Ver los proximos eventos
python3 -c "from google_helpers import headers; import requests; print(requests.get('https://www.googleapis.com/calendar/v3/calendars/primary/events?maxResults=3&singleEvents=true&orderBy=startTime&timeMin=$(date -u +%Y-%m-%dT%H:%M:%SZ)', headers=headers()).json().get('items', [])[:3])"
[ ] CRM: Listar contactos
python3 ~/assistant/scripts/ghl_helper.py contacts
[ ] VOZ (PWA): Abrir https://tudominio.com/app/ en el movil y enviar un mensaje
[ ] VOZ (Alexa): "Alexa, abre mi asistente" y preguntarle algo
[ ] DASHBOARD: Abrir https://tudominio.com/dashboard/
[ ] SEGURIDAD: Conectar por SSH con el nuevo puerto
```
### Resumen de todo lo configurado
Ejecuta esto para ver tu resumen completo:
```bash
cat ~/.assistant-config.json | python3 -m json.tool
```
---
## FASE 10: Generar tu CLAUDE.md personalizado
Basandome en todo lo que hemos configurado, genero automaticamente tu archivo CLAUDE.md. Este archivo es lo que le dice a Claude Code quien eres, que herramientas tiene disponibles, y como debe comportarse.
```bash
cat > ~/CLAUDE.md << 'CLAUDEMD'
# Asistente Personal de NOMBRE
Eres el asistente personal de NOMBRE. Tu trabajo es ayudarle a ser mas organizado y productivo.
## Contexto
- **Nombre:** NOMBRE
- **Actividad:** NEGOCIO
- **Timezone:** (configurar)
## Herramientas disponibles
### Notas (APP_NOTAS)
- Para escribir notas: `python3 ~/assistant/scripts/obsidian-write.py "ruta/nota.md" "contenido"`
- Para leer notas: acceder a ~/obsidian-vault/
### Calendario (CALENDARIO)
- Helper: ~/assistant/scripts/google_helpers.py
- Puede leer eventos del calendario
### CRM (CRM)
- Helper: ~/assistant/scripts/ghl_helper.py
- Puede listar contactos, buscar, y ver pipelines
### Dashboard
- Script: ~/assistant/scripts/generate_dashboard.py
- Output: ~/assistant/dashboard/index.html
- Se actualiza automaticamente cada hora
## Reglas
- Hablar siempre en espanol
- Tono informal y directo
- Sin emojis nunca
- Directo al grano, sin relleno
## Credenciales
Todas en ~/assistant/credentials/ (NUNCA mostrar contenidos)
## Scripts
Todos en ~/assistant/scripts/
CLAUDEMD
```
Yo personalizo este archivo con todos los datos reales de tu configuracion.
---
## Resolucion de problemas comunes
### "No puedo conectarme por SSH"
```bash
# En el servidor, verificar que SSH esta corriendo
sudo systemctl status sshd
# Ver si hay firewall bloqueando
sudo ufw status
# Ver intentos de conexion
sudo tail -20 /var/log/auth.log
```
### "Obsidian Sync no sincroniza"
```bash
# Ver el estado del servicio
sudo systemctl status obsidian-sync
sudo journalctl -u obsidian-sync -n 50
# Reiniciar
sudo systemctl restart obsidian-sync
```
### "La app de voz no carga"
```bash
# Verificar que el servidor Flask esta corriendo
sudo systemctl status voice-assistant
sudo journalctl -u voice-assistant -n 20
# Verificar nginx
sudo nginx -t
sudo systemctl status nginx
```
### "El certificado SSL falla"
```bash
# Renovar
sudo certbot renew
# Si falla, regenerar
sudo certbot --nginx -d TU_DOMINIO.COM
```
### "Claude no puede escribir notas"
```bash
# Verificar que el servicio de sync esta corriendo
sudo systemctl status obsidian-sync
# Verificar que el directorio del vault existe
ls ~/obsidian-vault/
# Test manual
python3 ~/assistant/scripts/obsidian-write.py "Test/debug.md" "Test de debug"
# Verificar que la nota se creo
cat ~/obsidian-vault/Test/debug.md
```
---
## Que sigue?
Ahora que tienes tu asistente configurado, puedes:
1. **Pedirle que te organice el dia** -- "que tengo hoy en el calendario?" / "apunta que tengo que llamar a X"
2. **Crear automations** -- scripts que corren solos (ej: resumen diario a las 9am)
3. **Conectar mas herramientas** -- Stripe, Google Ads, Meta Ads, lo que necesites
4. **Crear Skills** -- flujos de trabajo recurrentes que Claude ejecuta con un comando
Tu asistente aprende y mejora contigo. Cuanto mas lo uses, mejor te conoce.
---
_Generado con Claude Code. Basado en la infraestructura real de KERS Agency._