From 0c75d948128672bb786383b36b6578e38fd21c17 Mon Sep 17 00:00:00 2001 From: Bjorn Blomberg Date: Sun, 28 Jun 2026 01:53:50 +0200 Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20arkitektur,=20datafl=C3=B6d?= =?UTF-8?q?e=20och=20fallback-navigering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitignore | 32 +++++++++ README.md | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3c55b5 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index 3c36cf1..097835d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,190 @@ # agent-helm -egen gjord remote sesson handler for googel ai \ No newline at end of file +> 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`