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>
This commit is contained in:
2026-06-28 01:53:50 +02:00
parent 9b87955dc3
commit 0c75d94812
2 changed files with 220 additions and 1 deletions

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Dependencies
node_modules/
.pnpm-store/
# Build output
dist/
build/
*.tsbuildinfo
# Vite / PWA
packages/web/dev-dist/
# Logs
*.log
npm-debug.log*
pnpm-debug.log*
# Env & secrets (API-nycklar, ntfy-topics, tokens)
.env
.env.*
!.env.example
# Editor / OS
.DS_Store
.idea/
.vscode/
*.swp
# Runtime / session-state
*.pid
scrollback/
sessions/

189
README.md
View File

@@ -1,3 +1,190 @@
# agent-helm
egen gjord remote sesson handler for googel ai
> 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](#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](#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`