Bjorn Blomberg 0c75d94812 Initial commit: arkitektur, dataflöde och fallback-navigering
README beskriver agent-helm — fjärrstyrning av Gemini CLI-sessioner från
mobil/webb. Dokumenterar komponenter (daemon/server/web/hooks), repo-struktur,
dataflöde, de två åtskilda input-kanalerna, samt fallback-navigering
(piltangenter + Enter rakt till pty) så att sessioner kan styras oavsett om
BeforeTool/Notification-hooks är uppsatta eller fungerar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 01:59:54 +02:00
2026-06-28 01:56:25 +02:00

agent-helm

Fjärrstyr CLI-agent-sessioner (Gemini CLI först) från mobil eller webb — med en alltid-fungerande terminalkanal i botten och snygga, strukturerade frågekort som bonus ovanpå.

agent-helm låter dig köra en agent (Gemini CLI) på valfri maskin, strömma dess session till en server på din Pi5, och styra/svara på den från en webbfrontend (installbar PWA) i mobilen. Du ser alla anslutna instanser på ett ställe och kan hoppa in i vilken som helst, var du än är.


Designprinciper

  1. Graceful degradation — terminalen funkar alltid. Den råa terminalkanalen (tangenttryck → pty) är baslinjen och fungerar oavsett konfiguration. Hooks som ger snygga frågekort är progressive enhancement — om en BeforeTool/Notification-hook saknas, är felkonfigurerad eller kraschar, kan du fortfarande styra sessionen fullt ut genom att skicka piltangenter + Enter (och all annan text) rakt in i terminalen. Se Fallback-navigering.

  2. Två tydligt åtskilda input-vägar. Fritext/tangenttryck går till pty:ns stdin. Strukturerade hook-svar (allow/deny) går tillbaka via hookens returvärde — aldrig genom att "skriva y i terminalen". Se Input-kanaler.

  3. pty äger agenten, inte tmux. Appen kör Gemini i en node-pty. Persistens är ett valfritt yttre lager (tmux eller Docker) — inte en del av integrationen.

  4. Servern äger sessionens minne. En scrollback-buffert per session gör att en mobil som återansluter ser var den är, inte en tom skärm.


Komponenter

Komponent Roll Teknik
daemon (packages/daemon) "Appen". Spawnar Gemini i en node-pty, strömmar pty-bytes utåt över WebSocket, tar emot input och skriver till pty:n. Levererar även hook-frågor och släpper dem när svar kommer. Node.js, node-pty, ws
server (packages/server) Control-plane på Pi5. Session-registry (daemon ↔ session-id), WebSocket-mux mot både daemons och frontends, scrollback per session (headless xterm + SerializeAddon), routing av input till rätt session. Node.js, ws, @xterm/headless
web (packages/web) PWA-frontend. xterm.js för konversationen, kort-vy för hook-frågor, input-toolbar för fallback-navigering, lista över alla anslutna instanser. Vue 3, Vite, @xterm/xterm, PWA
hooks (hooks/) Gemini CLI-hooks (BeforeTool, Notification) som postar frågan till daemonen och long-pollar tills ett svar kommer. Shell/Node-script, ~/.gemini/settings.json

Repo-struktur

agent-helm/
├── README.md
├── .gitignore
├── package.json              # workspace-rot (npm/pnpm workspaces)
├── packages/
│   ├── daemon/               # node-pty-värd för Gemini + WS-klient mot servern
│   │   ├── src/
│   │   └── package.json
│   ├── server/               # Pi5 control-plane: WS-mux, registry, scrollback
│   │   ├── src/
│   │   └── package.json
│   └── web/                  # Vue 3 PWA: xterm.js + kort-vy + fallback-input
│       ├── src/
│       └── package.json
├── hooks/
│   ├── before-tool.sh        # postar verktygsförfrågan till daemonen, blockar
│   └── notification.sh       # postar agent-notiser (t.ex. "väntar på input")
└── docker/
    ├── daemon.Dockerfile
    ├── server.Dockerfile     # byggs och pushas till 192.168.0.19:5000
    └── compose.pi5.yml       # server + ntfy, restart: unless-stopped

Dataflöde

[daemon / "appen"]  (körs valfritt i tmux eller Docker för persistens)
   node-pty ── spawnar ── gemini  (interaktivt TUI)
        │                    └─ BeforeTool/Notification-hook (körs HÄR, daemon-side)
        │ pty-bytes (stdout)        │ POST {tool_name, tool_input}, blockar tills svar
        ▼                           ▼
   ───── WebSocket (utåtgående) ──────────────────►  [Pi5-server / control-plane]
   ◄──── input (text / tangenter / hook-svar) ──────  • session-registry: daemon ↔ session-id
                                                       • headless xterm + SerializeAddon → scrollback
                                                       • router:
                                                            text/tangenter → pty.write()
                                                            hook-svar      → släpper blockad hook
                                                              │
                                                  WebSocket  ▼
                                                       [Vue 3 PWA]
                                                         • lista över alla anslutna instanser
                                                         • xterm.js   → konversationen (rå, alltid)
                                                         • kort-vy    → hook-frågor (allow/deny)
                                                         • toolbar    → ↑↓←→ Enter Esc Tab Ctrl-C

Steg för steg (en verktygsförfrågan):

  1. Gemini vill köra ett verktyg och fyrar BeforeTool-hooken på daemon-sidan.
  2. Hooken POST:ar {tool_name, tool_input, session_id} till daemonen och blockar (long-poll) tills ett beslut kommer.
  3. Daemonen vidarebefordrar frågan till servern → servern pushar till alla frontends som tittar på sessionen (och triggar en ntfy-push till mobilen).
  4. Du trycker Tillåt / Neka i kort-vyn → frontend → server → daemon → hooken släpps och returnerar {"decision":"allow"} → Gemini fortsätter.
  5. Om hooken inte är uppsatt/fungerar: inget kort dyker upp. Istället visar Gemini sin egen y/n-prompt i terminalen, och du svarar via fallback-navigeringen (se nedan).

Input-kanaler

Två vägar, alltid åtskilda:

Input Väg Hamnar i
Fritext, piltangenter, Enter, Esc, Tab, Ctrl-C frontend → server → daemon → pty.write() terminalens stdin
Svar på hook-fråga (allow / deny / val) frontend → server → släpper den blockade hooken hookens returvärde

⚠️ Strukturerade godkännanden skickas aldrig genom att skriva i terminalen — det skapar race mot TUI:ns egen prompt. De går tillbaka via hookens returvärde.


Fallback-navigering

Detta är ett förstklassigt krav, inte en reservplan: frontenden ska alltid kunna styra sessionen även om ingen hook är inblandad.

  • Frontenden har en input-toolbar med ↑ ↓ ← →, Enter, Esc, Tab, Ctrl-C (plus snabbknappar y/n). Dessa skickar motsvarande escape-sekvenser in i pty:n via pty.write() (t.ex. piltangent upp = \x1b[A, Enter = \r).
  • xterm.js vidarebefordrar dessutom riktiga tangenttryck från ett fysiskt tangentbord rakt till samma kanal.
  • Resultat: när Gemini visar sin inbyggda TUI-prompt (verktygsgodkännande, val i en meny, bekräftelse) kan du navigera och svara helt utan hooks. Hooks gör bara frågorna snyggare — de är aldrig en förutsättning för att kunna styra sessionen.

Praktiskt innebär det att man kan börja använda systemet innan några hooks är konfigurerade, och att en trasig hook aldrig låser en session.


Persistens

  • Process-persistens (valfritt): kör daemonen i tmux för ad-hoc ("överlever att jag stänger SSH/laptop"), eller som Docker-container med restart: unless-stopped / systemd-unit för alltid-på på Pi5. tmux är inte en del av integrationen — bara ett yttre skal.
  • Vy-persistens (alltid på): servern håller en scrollback-buffert per session via en headless xterm + SerializeAddon. En frontend som ansluter (eller en mobil som väcks ur viloläge) får en ögonblicksbild + live-svans, inte en tom skärm.

Åtkomst, auth & notiser (Pi5)

  • Åtkomst: primärt via Tailscale (inget exponerat publikt). Alternativt via din NPM-reverse-proxy på t.ex. agent.brasse-pc.eu med auth framför.
  • Notiser: self-hostad ntfy på Pi5 pushar till mobilen när en session väntar på input. Servern postar till ett ntfy-topic; PWA:n / ntfy-appen tar emot.
  • Registry: server- och daemon-images byggs och pushas till 192.168.0.19:5000.

Teknikstack

  • Node.js överallt på backend (node-pty, ws, @xterm/headless).
  • Vue 3 + Vite för frontend, @xterm/xterm för terminalrendering, PWA för mobil-installation och push.
  • Docker (compose) för Pi5-deploy, ntfy för push, Tailscale för åtkomst.
  • Mål-agent: Gemini CLI (headless/hooks), men daemon-lagret är agent-agnostiskt och kan wrappa vilken pty-baserad CLI-agent som helst.

Status

🚧 Skelett / design. Detta repo innehåller hittills arkitektur och dataflöde. Nästa steg: scaffolda packages/daemon, packages/server, packages/web och hooks/.

Roadmap

  • Monorepo-uppsättning (workspaces) + tomma paket
  • daemon: node-pty spawnar Gemini, strömmar pty över WS
  • server: session-registry + WS-mux + scrollback
  • web: xterm.js-vy + input-toolbar (fallback-navigering) + instans-lista
  • hooks: before-tool long-poll + kort-vy i frontend
  • ntfy-integration + Tailscale-/NPM-deploy på Pi5
  • Docker-images → 192.168.0.19:5000
Description
egen gjord remote sesson handler for googel ai
Readme Apache-2.0 234 KiB
latest Latest
2026-06-28 18:04:21 +02:00
Languages
TypeScript 67.4%
Vue 27.4%
Shell 2.9%
Dockerfile 1.5%
CSS 0.5%
Other 0.3%