# AudioPen → PKB setup Voice-note capture pipeline. AudioPen records on phone, polishes on their cloud, POSTs the polished prose to your own infra. This vault's `inbox/raw/` receives the drop; `scripts/audiopen-ingest.sh` normalises frontmatter and moves it to `inbox/`; `/triage-inbox` routes it into `daily/`, `notes/`, `projects/`, or `reading/`. Two ingest paths are supported. Pick one. Either works with the same `/triage-inbox` downstream. --- ## Path A — Webhook via Tailscale Funnel (recommended) **Requires.** Tailscale installed on the desktop with Funnel enabled on your tailnet. Tailscale Funnel provides an outbound-initiated HTTPS endpoint (`*.ts.net`) with no router configuration and no exposed ports. Nothing sits behind Authentik; AudioPen hits the Funnel URL directly. ### 1. Generate a shared secret ```sh head -c 24 /dev/urandom | base64 | tr -d '/+=' > /tmp/secret cat /tmp/secret ``` Store it privately; it becomes part of the webhook URL. ### 2. Install the receiver systemd unit ```sh mkdir -p ~/.config/audiopen-webhook ~/.config/systemd/user printf 'AUDIOPEN_WEBHOOK_SECRET=%s\n' "$(cat /tmp/secret)" > ~/.config/audiopen-webhook/env chmod 600 ~/.config/audiopen-webhook/env cp /var/mnt/data/projects/council/scripts/audiopen-webhook/audiopen-webhook.service.example \ ~/.config/systemd/user/audiopen-webhook.service systemctl --user daemon-reload systemctl --user enable --now audiopen-webhook.service systemctl --user status audiopen-webhook.service # should show active (running) ``` Tail logs: ```sh journalctl --user -u audiopen-webhook.service -f ``` ### 3. Install the ingest path + service units ```sh # audiopen-ingest.service — normalises raw drops cat > ~/.config/systemd/user/audiopen-ingest.service <<'EOF' [Unit] Description=Normalise AudioPen drops into council/inbox/ [Service] Type=oneshot ExecStart=/var/mnt/data/projects/council/scripts/audiopen-ingest.sh Environment=COUNCIL_REPO=/var/mnt/data/projects/council EOF # audiopen-ingest.path — fires the service whenever inbox/raw/ gains a file cat > ~/.config/systemd/user/audiopen-ingest.path <<'EOF' [Unit] Description=Watch council/inbox/raw for new AudioPen drops [Path] PathChanged=/var/mnt/data/projects/council/inbox/raw Unit=audiopen-ingest.service [Install] WantedBy=default.target EOF systemctl --user daemon-reload systemctl --user enable --now audiopen-ingest.path ``` ### 4. Expose the receiver via Tailscale Funnel ```sh tailscale funnel --bg 8765 tailscale funnel status # show the public URL ``` Your endpoint is now `https://..ts.net:443/`. AudioPen will POST to `https://..ts.net/audiopen/`. ### 5. Configure AudioPen In AudioPen (web or mobile app): settings → integrations → add a generic webhook. - **URL:** `https://..ts.net/audiopen/` - **Method:** POST - **Content-Type:** application/json - **Trigger:** after each recording is polished AudioPen typically sends JSON with keys like `title`, `body`, `orig_transcript`. The receiver accepts any of these; if AudioPen changes shape, check `journalctl --user -u audiopen-webhook.service` for the logged payload and update `scripts/audiopen-webhook/main.py::extract_content()` if needed. ### 6. Test end-to-end ```sh SECRET=$(awk -F= '/AUDIOPEN_WEBHOOK_SECRET/ {print $2}' ~/.config/audiopen-webhook/env) FUNNEL=$(tailscale funnel status | awk '/https:/ {print $1; exit}') curl -X POST -H 'Content-Type: application/json' \ -d '{"title":"Test","body":"Hello from curl. This is a test drop."}' \ "${FUNNEL%/}/audiopen/${SECRET}" # → ok ls /var/mnt/data/projects/council/inbox/raw # should contain the test file briefly sleep 2 ls /var/mnt/data/projects/council/inbox # should now contain the normalised file ``` Then record an actual voice note in AudioPen, wait for polishing, confirm the same flow. Run `/triage-inbox` in Claude Code when you're ready to route the contents. --- ## Path B — Email → IMAP fetch (no-open-ports fallback) Use this if you don't run Tailscale / Cloudflare Tunnel and don't want to stand one up. Fully outbound: AudioPen emails each note; a cron on the desktop pulls via IMAP and drops files in `inbox/raw/`. ### 1. Dedicated email address Create or repurpose a mailbox for AudioPen's output. Options: - Fastmail alias → Fastmail IMAP on desktop (works). - Self-hosted mail server → pull via IMAP from localhost (works). - Gmail "+audiopen" alias → fine but adds Google to the dependency chain. ### 2. IMAP fetch script (stub — you'll customise for your mail server) ```python #!/usr/bin/env python3 """Minimal IMAP fetcher: pulls unread messages from a mailbox, writes them as bare markdown to inbox/raw/. Uses stdlib imaplib. Keep credentials in ~/.config/audiopen-mail/env or similar.""" import imaplib, email, os, re, pathlib, datetime HOST = os.environ["IMAP_HOST"] USER = os.environ["IMAP_USER"] PASS = os.environ["IMAP_PASS"] MBOX = os.environ.get("IMAP_MAILBOX", "INBOX") INBOX = pathlib.Path(os.environ.get("COUNCIL_INBOX_RAW", "/var/mnt/data/projects/council/inbox/raw")) INBOX.mkdir(parents=True, exist_ok=True) m = imaplib.IMAP4_SSL(HOST) m.login(USER, PASS) m.select(MBOX) typ, data = m.search(None, 'UNSEEN') for num in data[0].split(): typ, msg_data = m.fetch(num, '(RFC822)') msg = email.message_from_bytes(msg_data[0][1]) title = (msg.get("Subject") or "Untitled").strip() body = "" if msg.is_multipart(): for p in msg.walk(): if p.get_content_type() == "text/plain": body = p.get_payload(decode=True).decode('utf-8', errors='replace') break else: body = msg.get_payload(decode=True).decode('utf-8', errors='replace') slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")[:60] or "untitled" ts = datetime.datetime.now().strftime("%Y-%m-%d-%H%M") (INBOX / f"{ts}_{slug}.md").write_text(f"# {title}\n\n{body}\n") m.store(num, '+FLAGS', '\\Seen') m.logout() ``` Save as `scripts/audiopen-imap-fetch.py`. Run it every N minutes via a systemd user-timer or cron. The same `audiopen-ingest.path` unit (from Path A, step 3) picks up the drops from there. ### 3. Configure AudioPen Settings → integrations → email output → recipient = your dedicated address. Each recording → one email. Done. --- ## Routine Once everything is wired: 1. **Capture**: voice note → AudioPen → (webhook or email) → desktop. 2. **Ingest**: automatic — you don't run anything; systemd fires on file change. 3. **Route**: when you want, open a Claude Code session in the vault and run `/triage-inbox`. Koskela proposes destinations; you approve in batch. 4. **Weekly/monthly**: the promoted notes are already in their homes; `/reflect`, `/review-week`, and `/review-month` will see them naturally. ## Troubleshooting | Symptom | Check | |---|---| | Files pile up in `inbox/raw/` but never normalise | `systemctl --user status audiopen-ingest.path audiopen-ingest.service` — path unit enabled? service errored? Run the script manually once: `bash scripts/audiopen-ingest.sh`. | | Webhook POSTs 200 but no file lands | Check the systemd logs for the receiver: `journalctl --user -u audiopen-webhook.service -n 50`. The logged path should be writable and under `inbox/raw/`. | | Webhook POSTs 404 | Secret mismatch between AudioPen URL and `~/.config/audiopen-webhook/env`. Regenerate and re-set both sides. | | Funnel URL unreachable | `tailscale status`; `tailscale funnel status`. Re-run `tailscale funnel --bg 8765`. Confirm Funnel is enabled on your tailnet (admin → DNS → HTTPS Certificates; admin → Access controls → Funnel). | | Normalised file missing frontmatter | Bug in the ingest wrapper; open the raw file, run the script with `bash -x scripts/audiopen-ingest.sh` to trace. |