Tornar al blog

Intel·ligència Artificial

Com construir el teu assistent personal IA pas a pas amb Claude Code, MCP i un bot de Telegram

Construeix el teu assistent personal IA amb Claude Code i MCP — hero

Per l'equip de Kiwop · Agència Digital especialitzada en Desenvolupament de Programari i Intel·ligència Artificial aplicada per a clients globals a Europa i els EUA · Publicat el 19 d'abril de 2026 · Última actualització: 19 d'abril de 2026

TL;DR — Tutorial pas a pas per muntar el teu assistent personal IA sobre Claude Code MCP com a stack base: quatre canals (IMAP, Gmail, Slack, Telegram), briefings automàtics a les 08:00 i 19:00, bot bidireccional i comandes slash. Unes 500 línies de Python, zero dependències de pagament més enllà de la subscripció Claude Max x20. Setup complet en 60-90 minuts seguint els passos.

Vols que ho fem per tu? Mira el nostre servei de Consultoria IA

Construeix el teu assistent personal IA amb Claude Code i MCP — hero

Aquest és el segon article de la sèrie sobre el PA de Kiwop. El primer, D'OpenClaw a PA amb Claude Code i MCP, explica per què el vam construir després del canvi de política d'Anthropic del 4 d'abril de 2026. Aquest tutorial mostra com el vam construir, amb el codi real que tenim en producció des de fa dues setmanes. En acabar, tindràs un assistent personal IA funcional a la teva màquina, amb la mateixa arquitectura que fem servir diàriament a l'agència. El repositori de referència és a https://github.com/purroy/majordomo.

No és una demo de joguina. Processa correu de producció, agenda real, missatges de Telegram des del mòbil i briefings que aterren a les 08:00 i a les 19:00 sense intervenció humana. La filosofia és minimalista a propòsit: cada peça fa una sola cosa, el pegament entre peces és Unix, i els secrets mai toquen el disc en clar.

Què construiràs

Al final del tutorial, el teu PA tindrà quatre canals operatius i una arquitectura deliberadament simple.

Els quatre canals actius del PA són:

  • Correu IMAP/SMTP propi: per a la bústia corporativa del teu domini (mail.tevaempresa.com), llegit amb scripts Python sobre la llibreria estàndard de Python. Lectura, triatge, redacció i enviament amb confirmació explícita.
  • Google Workspace (Gmail + Calendar + Drive): a través dels MCP servers oficials — pots afegir-ne d'altres similars com Outlook/Microsoft 365 si la teva organització els fa servir.
  • Slack: DMs i mencions, via MCP server oficial de Slack per a Claude.
  • Telegram: un bot bidireccional que és alhora canal de sortida (briefings) i d'entrada (parles amb el PA des del mòbil).

L'stack tècnic són quatre capes. Claude Code com a motor conversacional. Connectors MCP per a tot el que en té un d'oficial (Google + Slack). Scripts Python propis per als buits que MCP no cobreix (IMAP i Telegram). I launchd (a macOS) o systemd/Task Scheduler (a Linux/Windows) per a l'arrencada i els disparadors programats.

El cost marginal real és zero sobre la subscripció Claude Max x20 que ja pagues per a desenvolupament. Les ~500 línies de Python són teves — cap dependència instal·lable, només llibreria estàndard. No hi ha backend, no hi ha base de dades, no hi ha Docker. Tot l'estat persistent viu en arxius locals i al clauer del sistema operatiu.

A alt nivell, l'arquitectura segueix tres principis que convé explicitar abans d'entrar al codi. El primer és que Claude Code és el motor però no és el propietari de l'estat: els teus prompts viuen en arxius Markdown versionables al teu propi repositori, els teus secrets viuen al clauer del sistema, els teus briefings són arxius en disc. Si demà volguessis canviar de motor, només hauries de substituir les crides a claude --print per una altra CLI equivalent. El segon és que MCP fa la feina pesada quan existeix un servidor oficial (Google, Slack) i escrius codi només quan no n'hi ha (IMAP, Telegram). El tercer és que la seguretat viu en tres capes independents — OAuth del MCP, Keychain de macOS i regla de confirmació als prompts — de manera que comprometre'n una no compromet el sistema.

Prerequisits i cost real

Abans de començar, revisa que tens el que cal. El tutorial assumeix macOS perquè és el que fem servir nosaltres, però hi ha una secció al final sobre com portar-ho a Linux o Windows.

Requisits de compte i subscripció:

  • Claude Max x20 d'Anthropic: la subscripció "for power users", uns 200 USD/mes. Claude Code ve inclòs i és el motor del PA.
  • Compte de Google Workspace amb permisos per autoritzar Gmail, Calendar i Drive via OAuth.
  • Workspace de Slack on tinguis compte (no cal ser admin per al MCP de lectura dels teus propis DMs i mencions).
  • Un domini de correu sobre IMAP si la teva bústia principal no és Gmail (opcional — si tot el teu correu és Gmail, salta't el Pas 3).
  • Compte de Telegram (gratuït) per crear el bot via @BotFather.

Requisits de sistema:

  • macOS 14+ amb Homebrew instal·lat (per al flux base del tutorial).
  • Python 3.11+ (ve amb macOS recent; si no, brew install [email protected]).
  • Claude Code CLI instal·lat: npm i -g @anthropic-ai/claude-code o via brew.
  • Terminal amb permisos de Full Disk Access si vols que launchd pugui executar els briefings sense demanar autorització repetida.

Cost mensual projectat, desglossament honest:

ÍtemCostNotes
Claude Max x20~200 USD/mesCobreix dev + PA + briefings dins del pla
MCP servers (Google, Slack)0 USDOficials, gratuïts
Telegram Bot API0 USDGratis fins a volums industrials
Bot Telegram hosting0 USDCorre al teu Mac, no hi ha VPS
Domini + IMAPel que ja paguisReutilitzes la teva bústia existent
**Total marginal sobre Claude Max****0 USD/mes**—

El punt clau: el cost marginal del PA és zero si ja tens Claude Max. Si no la tens i només la compraries per al PA, el càlcul canvia — hauries de valorar si l'estalvi de temps et compensa 200 USD/mes. Per a qualsevol persona que ja faci servir Claude Code en desenvolupament, la resposta acostuma a ser sí de forma trivial.

El temps estimat per completar el tutorial des de zero, comptant els OAuth i el debug inevitable, és de 60 a 90 minuts. Si només vols els tres primers canals (sense Telegram ni briefings programats), baixes a 30-40 minuts.

Pas 1: Estructura base del projecte

El PA viu en un directori propi fora de qualsevol altre projecte. La primera decisió és on ubicar-lo. Nosaltres fem servir ~/Dev/PA per convenció — substitueix-lo si prefereixes una altra ruta. El repositori complet de referència és a https://github.com/purroy/majordomo — fes-lo servir si et perds en algun pas.

mkdir -p ~/Dev/PA
cd ~/Dev/PA
git init
mkdir -p scripts .claude/commands briefings drafts scripts/launchd
touch CLAUDE.md README.md .gitignore

El .gitignore ha d'excloure qualsevol cosa amb dades personals — briefings, esborranys de correu, estat del bot:

briefings/
drafts/
.telegram_state.json
.mail_watch_state.json
__pycache__/
*.pyc
.env
.env.local

Ara l'arxiu més important del repositori: CLAUDE.md. Defineix la persona de l'assistent, l'idioma per defecte, i la regla de confirmació que és el cor de la seguretat del PA.

# PA — Asistente personal

Eres el asistente personal de [tu nombre]. Trabajas en este repo como un
PA real: lees mi correo, mi agenda (Google Calendar), mi Drive y mi
Slack, y me ayudas a priorizar, redactar y mantener mi día bajo control.

## Reglas críticas (no negociables)

1. **Confirmación explícita antes de enviar correo o crear/mover
   eventos.**
   - Para correo: SIEMPRE escribe primero un borrador (sin `--yes`) y
     muéstralo. Solo ejecuta `mail_send.py --yes` cuando yo diga
     claramente "envía", "ok", "confirmo", "send it", etc.
   - Para Calendar: muestra el evento propuesto (título, fecha, hora,
     asistentes) y espera confirmación antes de `create_event`.
   - Para Slack: muestra el mensaje propuesto antes de enviar.
   - Una confirmación vale para UNA acción. No reutilices un "ok"
     anterior para una acción nueva.

2. **No inventes contactos, fechas ni asuntos.** Si no tienes el dato,
   pregúntalo.

3. **Bilingüe según contexto.**
   - Conversación conmigo → español por defecto.
   - Borradores de respuesta a correos → mismo idioma del original.

4. **Privacidad.** No envíes información del buzón/calendario a
   herramientas externas sin permiso explícito.

## Tono
Profesional pero cercano. Frases cortas. Sin emojis salvo que el
original los use.

La regla de confirmació és innegociable. Diferencia un PA que pots deixar en mans d'un model de llenguatge d'un que t'enviarà un correu vergonyós quan malinterpreti un "sí" de fa dos torns. L'hem provada durant tot març sense un sol fals positiu.

Commit inicial:

git add CLAUDE.md .gitignore README.md
git commit -m "PA: estructura inicial + CLAUDE.md"

Pas 2: Connectar MCP servers

MCP ([Model Context Protocol](https://modelcontextprotocol.io/specification/2026-07-28)) és l'estàndard obert que Anthropic va publicar a finals de 2024 perquè els models de llenguatge es connectin a eines externes de forma uniforme. Un MCP server és un procés que exposa un conjunt de funcions (llegir correus, cercar arxius, enviar missatges) a través d'un protocol comú; un MCP client (com Claude Code) l'arrenca i consumeix aquestes funcions com si fossin eines pròpies. Si vols aprofundir en per què MCP és la peça que desbloqueja els agents IA el 2026, tenim una anàlisi més àmplia a WebMCP: la teva web preparada per a agents IA.

Per al PA activem quatre MCP servers oficials, els quatre publicats per Anthropic/Claude o pels mateixos proveïdors:

  • Gmail MCP: lectura, cerca i composició de correus.
  • Google Calendar MCP: agenda, creació i modificació d'esdeveniments.
  • Google Drive MCP: cerca i lectura d'arxius.
  • Slack MCP: lectura de DMs, canals i enviament amb confirmació.

La forma més senzilla de configurar-los és a través de la interfície de Claude Code. Des de l'arrel del projecte (~/Dev/PA), llança:

claude

Dins la sessió interactiva de Claude Code, afegeix els MCP servers amb la comanda /mcp add per a cadascun. La comanda et llançarà el flux OAuth corresponent al navegador — autoritza amb el compte que vols que faci servir el PA, no necessàriament el teu compte personal principal.

/mcp add claude-ai-Gmail
/mcp add claude-ai-Google-Calendar
/mcp add claude-ai-Google-Drive
/mcp add claude-ai-Slack

Seguretat del flux OAuth. Un aspecte clau: els MCP servers oficials fan servir OAuth del navegador, no credencials en disc. El teu refresh_token el guarda el MCP server en els seus propis magatzems xifrats, no el PA. Això significa que si algú roba el teu .claude/ o el teu repositori, no s'emporta accés al teu Gmail — hauria de comprometre a més el magatzem del MCP server i, en última instància, el teu clauer macOS. La superfície d'atac queda acotada.

Un cop autoritzats els quatre, comprova que Claude Code els detecta:

/mcp list

Hauries de veure les eines disponibles agrupades per servei: mcp__claude_ai_Gmail__search, mcp__claude_ai_Google_Calendar__list_events, etc. La nomenclatura segueix el patró mcp__<server>__<tool>.

Diagrama de l'stack després del Pas 2 — Claude Code com a motor, MCPs oficials connectats, buit pendent per a IMAP i Telegram

En aquest punt ja pots preguntar al PA "què tinc avui al calendari?" o "busca al Drive el contracte amb el client X" i funciona. El que falta són els dos canals que MCP no cobreix bé: el correu IMAP corporatiu i Telegram.

Pas 3: Scripts Python per a IMAP i Keychain

Si tot el teu correu és a Gmail, aquest pas és opcional. Si, com nosaltres, la bústia principal corre sobre un servidor IMAP propi (mail.kiwop.com en el nostre cas), toca escriure el pont.

L'arquitectura dels scripts és deliberadament petita. Un mòdul intern _mail.py amb la lògica comuna (connexió IMAP, parseig, SMTP), i tres comandes mínimes que el PA pot invocar:

  • mail_fetch.py: llista UIDs recents o no llegits com a JSON.
  • mail_read.py: llegeix el cos complet d'un UID (sense marcar-lo com a llegit — usa BODY.PEEK).
  • mail_send.py: envia un correu o el desa com a esborrany.

Regla d'or sobre credencials. Mai desis la contrasenya IMAP en un arxiu del repo ni en una variable d'entorn persistent tipus ~/.zshrc. El risc no és teòric: cada vegada que comparteixes un script, fas un commit amb .env trackat o instal·les una extensió de VS Code que llegeix el teu entorn, estàs exposant secrets. A macOS tenim Keychain, un clauer xifrat del sistema operatiu accessible via la comanda security. Els scripts llegeixen la credencial al vol quan la necessiten i mai l'escriuen en disc.

Primer, desa les credencials al Keychain:

security add-generic-password -U -a "$USER" -s "PA-mail-user"      -w "[email protected]"
security add-generic-password -U -a "$USER" -s "PA-mail-pass"      -w "EL_TEU_PASSWORD"
security add-generic-password -U -a "$USER" -s "PA-mail-imap-host" -w "mail.teudomini.com"
security add-generic-password -U -a "$USER" -s "PA-mail-imap-port" -w "993"
security add-generic-password -U -a "$USER" -s "PA-mail-smtp-host" -w "mail.teudomini.com"
security add-generic-password -U -a "$USER" -s "PA-mail-smtp-port" -w "587"

Els ports habituals: 993 per a IMAP sobre SSL i 587 per a SMTP amb STARTTLS. Si el teu proveïdor fa servir SMTPS implícit, canvia a 465.

Ara el helper shell que llegeix i escriu al Keychain. L'anomenem scripts/keychain.sh:

#!/usr/bin/env bash
# Helpers para leer/escribir secretos del PA en macOS Keychain.
set -euo pipefail

pa_kc_get() {
  local name="$1"
  security find-generic-password -a "$USER" -s "PA-${name}" -w 2>/dev/null
}

pa_kc_set() {
  local name="$1" value="$2"
  security add-generic-password -U -a "$USER" -s "PA-${name}" -w "${value}"
}

pa_kc_has() {
  local name="$1"
  security find-generic-password -a "$USER" -s "PA-${name}" >/dev/null 2>&1
}

# Uso CLI: scripts/keychain.sh get mail-user
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  cmd="${1:-}"; name="${2:-}"; value="${3:-}"
  case "$cmd" in
    get)  pa_kc_get "$name" ;;
    set)  pa_kc_set "$name" "$value" ;;
    has)  pa_kc_has "$name" && echo "yes" || echo "no" ;;
    *)    echo "usage: $0 {get|set|has} <name> [value]" >&2; exit 2 ;;
  esac
fi

L'equivalent Python fa servir subprocess per invocar security:

# scripts/_mail.py (fragmento)
import subprocess

def get_secret(name: str) -> str:
    """Lee PA-<name> del Keychain. Fallback a env var PA_<NAME>."""
    env_name = "PA_" + name.upper().replace("-", "_")
    if env_val := os.environ.get(env_name):
        return env_val
    out = subprocess.run(
        ["security", "find-generic-password",
         "-a", os.environ["USER"], "-s", f"PA-{name}", "-w"],
        capture_output=True, text=True, check=True,
    )
    return out.stdout.strip()

El fallback a variable d'entorn és deliberat: quan portis això a Linux (systemd), allà security no existeix i faràs servir env vars exportades des del unit file, llegides des d'un arxiu amb permisos 600.

Amb el Keychain connectat, el mail_fetch.py queda clar. És un CLI que imprimeix JSON perquè Claude Code el parsegi fàcilment:

#!/usr/bin/env python3
"""List recent / unread mail as JSON.

Usage:
  mail_fetch.py [--folder INBOX] [--unread] [--since 24h|7d] [--limit 20]
"""
from __future__ import annotations
import argparse, json, sys
from _mail import (
    fetch_envelope, imap_connect, MailConfig, search_uids,
    select_folder, snippet, text_body, fetch_message, parse_since,
)

def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--folder", default="INBOX")
    ap.add_argument("--unread", action="store_true")
    ap.add_argument("--since", default=None, help='e.g. 24h, 7d')
    ap.add_argument("--limit", type=int, default=20)
    ap.add_argument("--with-body", action="store_true")
    args = ap.parse_args()

    conn = imap_connect(MailConfig.load())
    try:
        select_folder(conn, args.folder)
        uids = search_uids(
            conn, unread=args.unread,
            since=parse_since(args.since) if args.since else None,
        )
        uids = uids[-args.limit:][::-1]
        envs = fetch_envelope(conn, uids)
        out = []
        for uid in uids:
            msg = envs.get(uid)
            if not msg:
                continue
            item = {
                "uid": uid,
                "from": str(msg.get("From", "")),
                "subject": str(msg.get("Subject", "")),
                "date": str(msg.get("Date", "")),
            }
            if args.with_body:
                full = fetch_message(conn, uid)
                item["snippet"] = snippet(text_body(full), 200)
            out.append(item)
        json.dump(out, sys.stdout, ensure_ascii=False, indent=2)
    finally:
        try: conn.logout()
        except Exception: pass
    return 0

if __name__ == "__main__":
    sys.exit(main())

mail_send.py hi afegeix una salvaguarda important: sense el flag --yes, desa el missatge com a .eml a drafts/ i no l'envia. Només amb --yes explícit surt per SMTP. Això aplica la regla de confirmació del CLAUDE.md a nivell de codi, no només de prompt — fins i tot si el model se saltés la regla, l'script no enviaria.

Detall crític sobre `BODY.PEEK`. Quan el PA "llegeix" un correu per triar-lo, no l'ha de marcar com a llegit. Si el marques, perds la teva safata de pendents com a senyal visual al client de correu. La bandera IMAP que posa \Seen és BODY[]; la que no la posa és BODY.PEEK[]. Fes-la servir sempre a mail_read.py.

Pas 4: Bot de Telegram amb sessió persistent

El bot de Telegram és la peça més interessant del PA perquè inverteix el flux: en comptes que tu obris un terminal i escriguis a Claude, Claude rep els teus missatges des del mòbil.

El pla: un daemon Python que fa long polling contra l'API de Telegram (sense necessitat de webhook públic), i cada missatge que rep el reenvia a claude --print amb una sessió executada dins del repositori del PA. Claude executa el prompt amb totes les seves eines (MCP + scripts Python) i torna la resposta. El daemon la formata per a Telegram i la reenvia al xat.

Crear el bot triga dos minuts. Obre Telegram, parla amb @BotFather, llança /newbot, dona-li un nom i un username. Et torna un token tipus 123456:ABC-XYZ_.... Desa'l al Keychain i a continuació detecta el teu chat_id (l'ID de la conversa 1-a-1 entre tu i el bot):

security add-generic-password -U -a "$USER" \
  -s "PA-telegram-bot-token" -w "EL_TEU_TOKEN_AQUI"

# Ara obre Telegram, envia "hola" al teu bot, i executa:
TOKEN=$(security find-generic-password -a "$USER" \
  -s "PA-telegram-bot-token" -w)
curl -s "https://api.telegram.org/bot${TOKEN}/getUpdates" \
  | python3 -c "import json,sys; d=json.load(sys.stdin); \
      print(d['result'][-1]['message']['chat']['id'])"

security add-generic-password -U -a "$USER" \
  -s "PA-telegram-chat-id" -w "EL_TEU_CHAT_ID"

Ara el daemon. L'esquelet de telegram_bot.py és aquest:

#!/usr/bin/env python3
"""PA Telegram bot — long-poll forwarding messages to `claude -p`."""
from __future__ import annotations
import json, os, subprocess, sys, time, uuid
import urllib.parse, urllib.request
from pathlib import Path

REPO_DIR = Path(__file__).resolve().parent.parent
STATE = REPO_DIR / ".telegram_state.json"
LOG_DIR = REPO_DIR / "briefings"
LOG = LOG_DIR / "telegram_bot.log"

sys.path.insert(0, str(REPO_DIR / "scripts"))
from _mail import get_secret

POLL_TIMEOUT_S = 50
CLAUDE_TIMEOUT_S = 600
CHUNK = 4000  # Telegram hard limit is 4096


def tg(token, method, params=None, timeout=30):
    url = f"https://api.telegram.org/bot{token}/{method}"
    if params:
        body = urllib.parse.urlencode(params).encode()
        req = urllib.request.Request(url, data=body)
    else:
        req = urllib.request.Request(url)
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.load(r)


def send_message(token, chat_id, text):
    chunks = [text[i:i + CHUNK] for i in range(0, len(text), CHUNK)] or [""]
    for ch in chunks:
        tg(token, "sendMessage", {
            "chat_id": chat_id, "text": ch,
            "disable_web_page_preview": "true",
        })


def ask_claude(prompt: str) -> str:
    """Invoke claude --print with a fresh session per message."""
    cmd = [
        "claude", "--print",
        "--model", "sonnet",
        "--session-id", str(uuid.uuid4()),
        "--setting-sources", "project,user",
        "--output-format", "text",
        "--permission-mode", "bypassPermissions",
        prompt,
    ]
    out = subprocess.run(
        cmd, cwd=REPO_DIR, capture_output=True, text=True,
        timeout=CLAUDE_TIMEOUT_S,
    )
    return (out.stdout or "").strip() or "(sin respuesta)"


def main() -> int:
    token = get_secret("telegram-bot-token")
    allowed_chat = int(get_secret("telegram-chat-id"))

    offset = 0
    while True:
        resp = tg(token, "getUpdates", {
            "offset": offset + 1,
            "timeout": POLL_TIMEOUT_S,
            "allowed_updates": json.dumps(["message"]),
        }, timeout=POLL_TIMEOUT_S + 15)

        for update in resp.get("result", []):
            offset = update["update_id"]
            msg = update.get("message") or {}
            chat_id = msg.get("chat", {}).get("id")
            text = (msg.get("text") or "").strip()

            if chat_id != allowed_chat:
                continue  # drop mensajes de chats no autorizados
            if text == "/ping":
                send_message(token, chat_id, "pong")
                continue
            if text == "/reset":
                send_message(token, chat_id, "OK, sesión reiniciada.")
                continue

            reply = ask_claude(text)
            send_message(token, chat_id, reply)


if __name__ == "__main__":
    sys.exit(main())

Tres detalls no obvis del disseny.

Primer, whitelist de `chat_id`: la variable allowed_chat és el teu propi xat i qualsevol missatge d'un altre chat_id es descarta silenciosament. Si algú descobreix l'username del teu bot i li envia missatges, no li contesta. És la barrera més simple però més eficaç contra l'abús.

Segon, `--permission-mode bypassPermissions`: el daemon corre sense demanar confirmació per executar scripts del repo perquè no hi ha un humà al davant que pugui respondre. La seguretat no ve dels prompts de Claude Code, ve del fet que (a) el daemon només respon al chat_id whitelisted, (b) el PA té la regla de confirmació a CLAUDE.md, i (c) els scripts sensibles com mail_send.py exigeixen --yes a nivell de codi.

Tercer, sessions efímeres per missatge. Cada ask_claude genera un session-id nou amb uuid.uuid4(). Això significa que el bot no manté memòria de conversa entre missatges — cada petició parteix de zero amb el teu CLAUDE.md i les teves comandes carregades. La memòria "llarga" (les teves preferències, firma, contactes prioritaris) viu a memory/ com a arxius que Claude llegeix en iniciar cada sessió, no a la pròpia sessió. Això evita acumular context obsolet i fa el comportament molt predictible.

Markdown compatible amb Telegram. Telegram no renderitza Markdown estil GitHub (no entén taules, no entén enllaços amb claudàtors en molts contextos, interpreta malament certs caràcters). En comptes de lluitar amb parse_mode: MarkdownV2 — que t'obliga a escapar tots els caràcters especials i es trenca amb facilitat — fem servir un petit conversor a text pla llegible (md_to_telegram.py) que elimina la sintaxi Markdown preservant l'estructura.

Per provar-ho abans del launchd:

cd ~/Dev/PA
python3 scripts/telegram_bot.py

Obre Telegram, escriu /ping al teu bot. Hauria de respondre pong. Envia després "quina hora és?" — t'hauria de respondre amb l'hora actual consultant al sistema via sessió de Claude. Felicitats: ja tens un PA accessible des del mòbil.

Configuració del bot de Telegram — token, chat_id, daemon arrencat i primer /ping respost

Pas 5: Briefings programats amb launchd

[`launchd`](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html) és el gestor de processos i tasques programades de macOS. És l'equivalent a cron + systemd combinats en un sol sistema. Cada tasca es defineix en un arxiu XML (.plist) que diu a macOS què executar, quan, i què fer si falla.

Crearem tres launchd jobs:

  1. Bot de Telegram com a daemon permanent (KeepAlive: true).
  2. Briefing matinal cada dia a les 08:00.
  3. Briefing vespertí cada dia a les 19:00.

Primer el wrapper scripts/run_briefing.sh que llança Claude Code en mode no interactiu per generar el briefing:

#!/usr/bin/env bash
# Genera un briefing del PA (morning|evening) llamando a claude -p.
set -euo pipefail

KIND="${1:-morning}"
case "$KIND" in morning|evening) ;;
  *) echo "usage: $0 {morning|evening}" >&2; exit 2 ;;
esac

REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
DATE="$(date +%F)"
OUT_DIR="$REPO_DIR/briefings"
OUT_FILE="$OUT_DIR/${DATE}-${KIND}.md"
mkdir -p "$OUT_DIR"

cd "$REPO_DIR"

# claude -p: modo print (no interactivo)
# --setting-sources project,user: carga CLAUDE.md y .claude/settings.json
# --permission-mode bypassPermissions: cron no puede responder prompts
claude -p "/$KIND" \
    --setting-sources project,user \
    --output-format text \
    --permission-mode bypassPermissions \
  > "$OUT_FILE" 2> "$OUT_FILE.err"

# Empujar el resultado a Telegram si está configurado
if security find-generic-password -a "$USER" \
     -s "PA-telegram-bot-token" >/dev/null 2>&1; then
  TOKEN=$(security find-generic-password -a "$USER" \
            -s "PA-telegram-bot-token" -w)
  CHAT=$(security find-generic-password -a "$USER" \
           -s "PA-telegram-chat-id" -w)
  BODY=$(python3 "$REPO_DIR/scripts/md_to_telegram.py" < "$OUT_FILE")
  curl -s -o /dev/null \
    -d "chat_id=${CHAT}" --data-urlencode "text=${BODY}" \
    "https://api.telegram.org/bot${TOKEN}/sendMessage"
fi

echo "OK $OUT_FILE"

Ara el .plist del briefing matinal. Desa'l a scripts/launchd/com.kiwop.pa-briefing-morning.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.kiwop.pa-briefing-morning</string>

  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>/Users/TU_USER/Dev/PA/scripts/run_briefing.sh</string>
    <string>morning</string>
  </array>

  <key>WorkingDirectory</key>
  <string>/Users/TU_USER/Dev/PA</string>

  <key>EnvironmentVariables</key>
  <dict>
    <key>PATH</key>
    <string>/Users/TU_USER/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
    <key>USER</key>
    <string>TU_USER</string>
    <key>HOME</key>
    <string>/Users/TU_USER</string>
  </dict>

  <key>StartCalendarInterval</key>
  <dict>
    <key>Hour</key>
    <integer>8</integer>
    <key>Minute</key>
    <integer>0</integer>
  </dict>

  <key>RunAtLoad</key>
  <false/>

  <key>StandardOutPath</key>
  <string>/Users/TU_USER/Dev/PA/briefings/cron-morning.out.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/TU_USER/Dev/PA/briefings/cron-morning.err.log</string>
</dict>
</plist>

Clau important: el `PATH` explícit a EnvironmentVariables. launchd no hereta el teu PATH del shell, així que si no l'hi passes, claude no estarà disponible i el briefing fallarà silenciosament. Inclou /opt/homebrew/bin (Apple Silicon), /usr/local/bin (Intel) i ~/.local/bin (si vas instal·lar Claude Code via pip --user).

Duplica l'arxiu per al vespertí canviant morning per evening i l'hora a 19. I crea el del bot de Telegram:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.kiwop.pa-telegram</string>
  <key>ProgramArguments</key>
  <array>
    <string>/opt/homebrew/bin/python3</string>
    <string>/Users/TU_USER/Dev/PA/scripts/telegram_bot.py</string>
  </array>
  <key>WorkingDirectory</key>
  <string>/Users/TU_USER/Dev/PA</string>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>ThrottleInterval</key>
  <integer>15</integer>
  <key>StandardOutPath</key>
  <string>/Users/TU_USER/Dev/PA/briefings/telegram_bot.out.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/TU_USER/Dev/PA/briefings/telegram_bot.err.log</string>
</dict>
</plist>

El trio RunAtLoad: true + KeepAlive: true + ThrottleInterval: 15 vol dir: arrenca en iniciar sessió, reinicia si cau, i no intentis reiniciar més ràpid que cada 15 segons (per evitar bucles de fallada).

Per activar els tres .plist:

mkdir -p ~/Library/LaunchAgents
cp scripts/launchd/com.kiwop.pa-briefing-morning.plist ~/Library/LaunchAgents/
cp scripts/launchd/com.kiwop.pa-briefing-evening.plist ~/Library/LaunchAgents/
cp scripts/launchd/com.kiwop.pa-telegram.plist         ~/Library/LaunchAgents/

launchctl load ~/Library/LaunchAgents/com.kiwop.pa-briefing-morning.plist
launchctl load ~/Library/LaunchAgents/com.kiwop.pa-briefing-evening.plist
launchctl load ~/Library/LaunchAgents/com.kiwop.pa-telegram.plist

Per verificar que són actius:

launchctl list | grep com.kiwop.pa

Hauries de veure les tres entrades amb PID (per al bot, que corre contínuament) o amb - (per als briefings, que només corren a les seves hores).

Timeline d'execucions de launchd — bot 24/7, briefing matinal a les 08:00, briefing vespertí a les 19:00

Per disparar un briefing manualment sense esperar a les 08:00 (útil per a testing):

launchctl kickstart -k gui/$(id -u)/com.kiwop.pa-briefing-morning

Pas 6: Comandes slash /morning, /evening, /inbox, /reply

Les comandes slash de Claude Code viuen a .claude/commands/ com a arxius Markdown. Cada arxiu és un prompt template que Claude executa quan invoques /nom a la sessió. Són la forma més neta d'encapsular fluxos repetitius sense repetir el mateix prompt llarg cada cop.

Per al PA definim quatre comandes mínimes: /morning, /evening, /inbox, /reply. Aquest és l'exemple de .claude/commands/morning.md:

---
description: Briefing matutino — agenda + triage de correo + Slack + prioridades
---

Prepara un briefing matutino en español. Estructura clara, frases
cortas, accionable.

## Paso 1: recoge en paralelo

1. **Agenda de hoy** (Calendar MCP, `list_events`): zona horaria
   Europe/Madrid.
2. **Correo no leído últimas 24h** (lista de UIDs):
   ```
   python3 scripts/mail_fetch.py --unread --since 24h --limit 30 --with-body
   ```
3. **Slack**: mensajes recibidos en últimas 24h vía MCP.

## Paso 2: triage de correos (sin marcar como leído)

Para cada UID, si el snippet deja claro que es ruido, no lo abras. En
cualquier otro caso, lee el cuerpo con `mail_read.py UID`. Clasifica:

- 🔥 **Fuego**: web caída, cliente enfadado, deadline <24h.
- ⚠ **Importante**: pregunta concreta, pre-venta, factura/contrato.
- **Para revisar**: informativo.
- **Ruido**: agrupar.

## Paso 3: output

```markdown
# Buenos días — <fecha>

## Agenda de hoy
- HH:MM–HH:MM · Título · (lugar / asistentes)

## Correo (N no leídos)
🔥 **Fuego**: [UID] Remitente — Asunto · contexto
⚠ **Importante**: [UID] ...
**Para revisar**: [UID] ...
**Ruido**: N items

## Slack
🔥 ...
⚠ ...

## Prioridades sugeridas
1. ...
2. ...
3. ...
```

## Reglas duras

- NUNCA marcar correos como leídos.
- Lee con `mail_read.py` (BODY.PEEK), nunca con tools que setteen
  `\Seen`.
- Si una fuente falla, dilo al final del briefing, no abortes.

La comanda /reply és particularment important perquè aplica la regla de confirmació:

---
description: Redactar respuesta a un correo (con confirmación obligatoria)
---

Argumentos: `$ARGUMENTS` debería contener un UID.

## Pasos

1. Lee el original: `python3 scripts/mail_read.py <UID>`.
2. Redacta el borrador EN EL IDIOMA DEL ORIGINAL. Tono profesional
   cercano, sin saludos pomposos, longitud breve.
3. Guarda el borrador en `/tmp/pa_reply_<UID>.txt`.
4. Muéstralo:
   - Asunto (Re: ...)
   - Destinatario
   - Cuerpo completo
   - Pregunta: "¿Envío, edito o cancelo?"
5. Espera confirmación explícita:
   - "envía"/"ok"/"sí" → ejecuta:
     ```
     python3 scripts/mail_send.py \
       --to "<from-original>" \
       --in-reply-to <UID> \
       --body-file /tmp/pa_reply_<UID>.txt \
       --yes
     ```
   - "edita ..." → ajusta y vuelve al paso 4.
   - "cancela" → no envíes.

## Reglas duras
- NUNCA llames a `mail_send.py --yes` sin un "sí" claro en el turno
  actual.
- NUNCA marques el original como leído.

Fixa't en el patró: la comanda fa complir la regla de confirmació a nivell de prompt (pas 5), i l'script fa complir la regla a nivell d'execució (el --yes obligatori a mail_send.py). Doble capa. Si el model "oblida" el pas 5, l'script no envia. Si algú modifica l'script per saltar-se --yes, el prompt encara exigeix confirmació. És la mateixa filosofia de defensa en profunditat que recomanem en projectes d'integració de LLMs per a qualsevol acció amb efectes irreversibles.

Les quatre comandes base són suficients per al 90% de l'ús diari. Pots afegir-ne més: /schedule (crear reunions), /book (agendar des d'una invitació), /auth (diagnòstic de l'estat dels MCPs).

Arbre de comandes slash del PA — morning, evening, inbox, reply, schedule, book, auth

Pas 7: Provar el setup end-to-end

Amb tot muntat, toca una ronda de tests completa abans de donar-lo per funcional. Aquest és el checklist que fem servir a Kiwop cada cop que repliquem el setup en una altra màquina:

1. MCPs autenticats. En una sessió de Claude Code des de ~/Dev/PA:

/mcp list

Els quatre servers han d'aparèixer amb tools disponibles. Si algun diu "not authenticated", repeteix l'OAuth.

2. Correu IMAP funcional. Des del terminal:

cd ~/Dev/PA
python3 scripts/mail_fetch.py --unread --limit 3

Hauria de tornar un JSON amb els teus tres correus no llegits més recents. Si falla amb imaplib.IMAP4.error, revisa Keychain i ports.

3. Briefing manual. Des de sessió interactiva de Claude Code:

/morning

Genera un briefing complet. No ha de trigar més de 60 segons.

4. Bot de Telegram bidireccional. Des de Telegram, al teu bot:

  • /ping → pong (immediat)
  • "què tinc avui a l'agenda?" → resposta amb l'agenda real (30-40 segons)

5. Logs de launchd sense errors.

ls -la ~/Dev/PA/briefings/*.log
tail -20 ~/Dev/PA/briefings/telegram_bot.err.log

El telegram_bot.err.log ha de tenir una línia "Bot up. Allowed chat=...". Si hi ha un FATAL: secret read failed, revisa que Keychain té els ítems PA-telegram-bot-token i PA-telegram-chat-id.

6. Prova en fred del briefing programat.

launchctl kickstart -k gui/$(id -u)/com.kiwop.pa-briefing-morning
# Espera 60s i revisa:
cat ~/Dev/PA/briefings/$(date +%F)-morning.md

Ha d'haver generat el briefing del dia i empès una còpia al teu Telegram. Si Telegram no rep, revisa les credencials PA-telegram-* al Keychain.

Problemes comuns i com diagnosticar-los:

SímptomaCausa habitualFix
`claude: command not found` als logs de launchd`PATH` no configurat al `.plist`Afegir `EnvironmentVariables.PATH` amb `/opt/homebrew/bin`
MCPs apareixen però sense toolsToken OAuth expirat`/mcp add <server>` de nou per re-auth
`mail_fetch.py` timeoutFirewall corporatiu bloqueja 993Provar des d'una altra xarxa, verificar ports IMAP
Bot no contesta `/ping`Daemon no arrenca a `launchd``launchctl list \grep pa-telegram` + revisar `.err.log`
Briefings no surten a les 08:00Mac adormit a aquella hora`pmset` per programar despertar, o acceptar que només corre si estàs despert

Un cop passa els sis punts del checklist, el PA està llest per a ús diari. A partir d'aquí tot és refinar: afegir memory/ amb les teves preferències d'estil, afegir més comandes slash segons els fluxos que detectis repetitius, ajustar els prompts de morning.md i evening.md a l'output exacte que et resulta més útil.

Consell pràctic sobre els primers set dies. Després de muntar-lo, el PA farà coses subòptimes fins que descobreixis què li falta de context sobre tu. Nosaltres recomanem començar en mode "només lectura" la primera setmana: deixa que generi briefings, deixa-li triar l'inbox, però no facis servir /reply ni confirmis accions d'escriptura encara. Durant aquesta setmana aniràs detectant patrons a corregir — "classifica malament els correus d'aquest client perquè no sap que és VIP", "resumeix de més els missatges curts de Slack", "marca com a soroll coses que per a mi són importants". Totes aquestes correccions van a memory/triage_rules.md i al CLAUDE.md. A partir del dia setè o vuitè, quan el triatge estigui consistent al 85-90%, ja pots obrir l'ús d'escriptura amb /reply i començar a guanyar temps real. Saltar-se aquesta fase de calibració és l'error més comú i la causa principal d'abandonaments a la nostra mostra de persones a qui hem ajudat a muntar-lo.

Variants i escalat: Linux, Windows, i funcionant 24/7

El tutorial està escrit sobre macOS perquè és on treballem, però l'arquitectura és portable. Els quatre ajustos principals:

Gestor de processos.

PlataformaSubstitut de launchdNotes
Linux`systemd --user` unitsVeure exemple a baix
WindowsTask Scheduler + serveiMés fricció, viable
Servidor remot`systemd` (nivell system)El model més net per a 24/7

Un unit de systemd per al bot de Telegram a Linux seria aproximadament així (~/.config/systemd/user/pa-telegram.service):

[Unit]
Description=PA Telegram bot
After=network-online.target

[Service]
Type=simple
WorkingDirectory=%h/Dev/PA
Environment="PA_TELEGRAM_BOT_TOKEN=..."
Environment="PA_TELEGRAM_CHAT_ID=..."
ExecStart=/usr/bin/python3 scripts/telegram_bot.py
Restart=always
RestartSec=15

[Install]
WantedBy=default.target

Activació: systemctl --user daemon-reload && systemctl --user enable --now pa-telegram.service.

Magatzem de secrets.

macOSLinuxWindows
Keychain (`security`)Secret Service (`secret-tool`) / libsecretCredential Manager (`cmdkey`)

El fallback a variables d'entorn en un arxiu amb permisos 600 funciona a les tres plataformes i és el denominador comú si vols un únic codi per a tots els sistemes.

launchd vs systemd, comparativa honesta:

Dimensiólaunchd (macOS)systemd (Linux)
FormatXML `.plist`INI text pla
Schedule per hora`StartCalendarInterval`Timer unit (`OnCalendar=`)
Daemon persistent`KeepAlive: true``Restart=always`
Logs`StandardOutPath``journalctl --user -u ...`
Usabilitat CLI`launchctl` verbós`systemctl` més net
Integració OSMolt estretaMolt estreta

Fer-lo córrer 24/7 en un servidor remot? És viable i ho hem provat. El patró: un petit VPS Linux amb systemd executant el bot de Telegram i els briefings programats, més una connexió IMAP/SMTP a la bústia, més el CLI de Claude Code autenticat amb el teu compte Anthropic. Avantatges: el PA contesta encara que el teu portàtil estigui apagat. Desavantatges: l'OAuth dels MCP servers és més delicat sense navegador local (hauràs de fer el flux inicial a la teva màquina i copiar el refresh token), i qualsevol actualització del CLI de Claude Code requereix SSH i desplegament. Per a la majoria de professionals que fan servir el Mac cada dia, mantenir-lo en local és l'opció amb millor relació simplicitat/utilitat.

El salt a un PA multi-usuari (un que serveixi a un equip) no és una variant del mateix tutorial — és un SaaS i requereix decisions arquitectòniques molt diferents (multi-tenancy, autenticació, aïllament de sessions). Si t'interessa explorar aquesta ruta, l'abordem als projectes de desenvolupament d'agents IA a mida.

Dues extensions que hem vist funcionar a la pràctica quan el PA base ja està estable. La primera és un memory/ poblat amb arxius Markdown que Claude carrega a cada sessió: un triage_rules.md amb els teus criteris exactes per classificar correu, un writing_style.md amb exemples de correus escrits per tu perquè els esborranys sonin a la teva veu, un vip_contacts.md amb les adreces que mai han de caure a "soroll". La segona és un watcher de correu en segon pla (un daemon similar al bot de Telegram, però que en comptes d'escoltar Telegram escolta la teva bústia IMAP) que dispara una notificació quan arriba un correu marcat com a foc segons les teves regles — útil per a fluxos on no n'hi ha prou amb el briefing de les 08:00. Ambdues extensions es construeixen amb la mateixa filosofia que la resta: scripts curts, secrets al Keychain, sessions efímeres de Claude Code.

Preguntes freqüents

Quant costa realment construir aquest PA?

Si ja tens Claude Max x20 (uns 200 USD/mes), el cost marginal del PA és zero. Els MCP servers són gratuïts, el bot de Telegram corre al teu Mac sense hosting, i els scripts Python no requereixen dependències de pagament. Si no tens Claude Max, el càlcul canvia: els 200 USD/mes són la inversió fixa. El temps de construcció seguint aquest tutorial és de 60-90 minuts.

És segur donar accés al meu correu i calendari a un PA construït així?

Sí, amb les condicions explícites següents. Primer, els MCP servers oficials autentiquen per OAuth sense desar la teva contrasenya en disc — el token viu en un magatzem xifrat del propi MCP server. Segon, les credencials IMAP/SMTP estan al Keychain de macOS (clauer del sistema operatiu, xifrat), mai en arxius plans. Tercer, el bot de Telegram només respon al chat_id whitelisted i la regla de confirmació obligatòria a CLAUDE.md impedeix accions d'escriptura sense "sí" explícit. La superfície d'atac queda molt acotada, comparable a la de qualsevol client de correu d'escriptori.

Què passa amb la privacitat de les dades? Anthropic entrena amb els meus correus?

Anthropic declara públicament (política a 2026) que les dades enviades per API i per Claude Code no es fan servir per entrenar models. Per a contingut regulat (sanitari, legal amb secret professional, financer confidencial), convé revisar els termes específics vigents i considerar si un PA sobre infraestructura pròpia és més apropiat — cas en què una solució de RAG empresarial pot ser l'alternativa correcta.

Es pot integrar WhatsApp en aquest PA?

A data d'abril de 2026 no existeix un MCP oficial de WhatsApp. Hi ha bridges no oficials (Matrix, Beeper, la pròpia WhatsApp Business Cloud API de Meta) que es poden connectar com a canal addicional, però tots tenen caveats: els bridges no oficials poden violar ToS de Meta i ser inestables, i la Business Cloud API requereix un compte comercial verificat. La nostra recomanació pragmàtica és fer servir Telegram com a canal mòbil primari.

Aquest PA pot donar servei a diversos usuaris?

No, per disseny. Un PA és single-user: les seves credencials, la seva memòria, les seves preferències, i el seu CLAUDE.md apunten a una sola persona. Per a escenaris multi-usuari (un equip que comparteix un assistent comú) l'arquitectura correcta és un SaaS multi-tenant amb aïllament de dades per usuari — no una còpia del PA amb variables compartides. És una conversa totalment diferent.

Funciona sense connexió a internet?

No. Claude Code requereix connexió per consultar el model, els MCP servers fan servir APIs online (Google, Slack, Telegram), i IMAP/SMTP òbviament també. El PA descrit aquí és un client que orquestra serveis cloud — no un model local. Si necessites operació offline, és un redisseny complet amb un LLM local (Llama, Mistral) corrent a ollama o similar, amb restriccions de qualitat i rendiment notables.

Quant triga el PA a donar el seu primer briefing matinal?

El run_briefing.sh complet triga entre 40 i 90 segons segons quant correu hagis de triar. La part més lenta és el fetch IMAP amb --with-body (una connexió per UID), seguida del raonament del model sobre el triatge. Si la teva bústia és molt gran (>50 no llegits), pot pujar a 2-3 minuts. Si és la primera execució i el MCP de Calendar encara està escalfant-se, afegeix 5-10 segons extra.

Puc personalitzar-lo per al meu flux concret?

Sí, i és la filosofia del projecte. Els arxius clau a tocar: CLAUDE.md (persona, to, regles), .claude/commands/*.md (fluxos concrets), memory/ (preferències persistents, firma, contactes VIP, regles de triatge). El codi Python rarament s'ha de tocar tret que s'afegeixin comptes IMAP addicionals o s'integri un canal nou. Comença amb el setup base funcionant abans de personalitzar — és més barat aprendre on personalitzar quan ja veus què et sobra i què et falta.

Conclusió: 60 minuts entre tu i el teu primer PA operatiu

Fa dues setmanes que fem servir aquest PA a Kiwop i la conclusió més honesta que podem escriure és que el salt de productivitat real no ve del model — ve del disseny minimalista al voltant del model. Claude Code, Opus 4.7, els MCP servers: són la part cara que no construeixes tu. Les ~500 línies de Python, el grapat d'arxius Markdown, els tres .plist de launchd — això és el que fas tu, i és el que converteix les capacitats del model en un assistent útil per al teu dia.

Aquest tutorial ha cobert el camí complet des d'un directori buit fins a un PA operatiu amb quatre canals i briefings programats. El codi de referència que hem fet servir està publicat a https://github.com/purroy/majordomo sota una llicència permissiva — clona, fes-li fork, adapta'l. No és l'única manera de construir un PA el 2026, però és una que sabem que funciona perquè la fem servir cada matí a les 08:00 abans d'encendre el Mac de la feina.

Si mentre seguies el tutorial has trobat un buit on el teu cas concret necessita més que aquest stack base — integracions a mida amb el teu CRM, un canal de Teams, un MCP propietari per al teu ERP, o directament un assistent multi-usuari per a tot el teu equip — a Kiwop construïm aquestes peces com a part dels nostres serveis de desenvolupament d'agents IA i consultoria d'IA. El nostre punt de partida és sempre el mateix: capes fines, desacoblades, agnòstiques al proveïdor. No hi ha res en aquest tutorial que no puguis replicar tu mateix en una tarda, però si el temps és més car que el cost d'externalitzar, contacta amb el nostre equip i ho fem amb tu.

El PA que tens ara corrent al teu Mac és teu. Ningú pot tallar-lo de la nit al dia. I això, el 2026, és la propietat més valuosa que pot tenir un agent IA aplicat a la feina diària.

Vols implementar intel·ligència artificial al teu negoci?

T'ajudem a decidir quin procés automatitzar primer, amb quines dades i quant costa, abans de gastar en desenvolupament.

Descobreix el nostre servei de Consultoria IA

Parlem.

Consulta tècnica inicial

IA, seguretat i rendiment. Diagnòstic i proposta tancada per fases.

  • NDA disponible
  • Resposta <24h
  • Proposta per fases

La teva primera reunió és amb un Arquitecte de Solucions, no amb un comercial.

Parlar amb un arquitecte