npm package: @lyalindotcom/ai-portal
Description: Telegram bridge for running local Gemini CLI sessions from any project directory.
Latest version: 0.2.2

# Portal

Portal is a Telegram bridge for local `gemini` CLI sessions.
Run `portal` in any project directory to control Gemini through Telegram.

## Warning

- Portal is experimental and unsupported.
- Regular mode typically runs with YOLO approval.
- YOLO auto-approves actions and can be risky on your machine.
- Use at your own risk, ideally in disposable/test environments.

## Requirements

- Node.js 18+
- `gemini` CLI installed
- Gemini auth configured once manually on this machine
- A Telegram bot token

## Install

```bash
npm i -g @lyalindotcom/ai-portal
```

## Quick start

```bash
portal
```

In an interactive terminal, `portal` opens a launcher UI with arrow-key selection for:
- Use last config (one-click launch from last saved startup profile)
- Start action
- Session mode
- Gemini backend
- Preferred port
- Owner reset toggle
- ACP live trace toggle (ACP backend)

`Use last config` reuses the most recent startup profile (mode/backend/port/trace) saved in global config.

Then in Telegram:

1. Open chat with your bot
2. Send `/start`
3. Send `/connect <PIN>` (PIN is shown in terminal)
4. Send prompts

First run opens interactive setup and stores config in `~/.portal/config.json`.

## CLI commands

- `portal`
- `portal start`
- `portal config`
- `portal --change`
- `portal --reset-owner`
- `portal config --reset-owner`
- `portal --port 3001`
- `portal --mode regular`
- `portal --mode assistant`
- `portal --backend headless`
- `portal --backend acp`
- `portal --acp-live-trace`
- `portal --no-acp-live-trace`
- `portal --ui`
- `portal --no-ui`

## Telegram commands

- `/start`
- `/help`
- `/connect <PIN>`
- `/ping`
- `/status`
- `/newsession`

## Session modes

- `regular`
  - Default behavior.
  - Commonly YOLO approval mode.
- `assistant`
  - Read-only assistant mode.
  - Requires `GEMINI_API_KEY`.
  - Requires Gemini auth type set to API key in `~/.gemini/settings.json`.

## Gemini backends

- `headless` (default)
  - Runs one-shot Gemini CLI commands (`gemini -p ...`) per request.
  - Uses CLI `--resume` for session continuity.
- `acp` (experimental)
  - Runs Gemini CLI in `--experimental-acp` mode with a persistent ACP transport.
  - Streams tool/message events into Portal logs.
  - Falls back to a new ACP session if `loadSession` fails.
  - Optional Telegram live trace stream can be toggled with `--acp-live-trace` / `--no-acp-live-trace`.

## File uploads

- Send a document or photo with a caption describing the task.
- File is downloaded to local sandbox and passed to Gemini with its path.
- Max size default: `10 MB`.
- `image/*` uploads are allowed.
- Gemini can return generated files to Telegram using:
  - `<portal_send_file>`
  - `path: relative/path/from/sandbox.ext`
  - `caption: Optional caption`
  - `</portal_send_file>`

## Runtime behavior

- Health endpoint: `http://localhost:<port>/healthz`
- Runtime dir (per project): `./.portal/`
- Portal injects a runtime Gemini system settings file to enable a safety hook:
  - blocks destructive `run_shell_command` tool calls before execution
  - logs hook decisions to `./.portal/logs/gemini-hooks.jsonl`
- Stable machine ID + per-run instance ID shown in startup diagnostics and `/status`.
- PIN is reused for up to 30 minutes when possible.
- One active request per chat, one queued request max.
- 3 invalid PIN attempts shut down the server.
- Interactions are logged to terminal and JSONL file.
- ACP backend additionally logs per-event stream updates (`tool_call`, chunks, permission decisions).
- ACP live trace in Telegram is rendered as stage updates + boxed tool output previews.
- On startup, Portal announces its machine/instance ID to owner chat (if owner chat is configured).
- If Telegram polling conflict is detected (another machine running same token), Portal sends a conflict alert and exits.

## Architecture

Portal is a local wrapper around Gemini CLI with Telegram as the remote control surface.

### High-level topology

```text
                     Telegram Cloud
                           |
                    getUpdates/send*
                           |
                    +-------------+
                    |  Poller +   |
                    |   Handler   |
                    +------+------+        +----------------------+
                           |               |  Express /healthz    |
                   owner/PIN gate          +----------------------+
                           |
                   +-------v-------+
                   |  Portal Core  |
                   | queue/session |
                   +-------+-------+
                           |
                    spawn gemini CLI
                           |
                   +-------v-------+
                   | Gemini Runner |
                   | headless/acp  |
                   +-------+-------+
                           |
                 .portal/agent_sandbox
```

### Main components

- CLI entrypoint
  - Parses args, runs first-time config wizard, starts server in current folder.
- Express health server
  - Serves `/healthz` for local diagnostics.
- Telegram client + poller
  - Long-polls `getUpdates`, handles reconnect/timeout recovery, detects conflict.
- Telegram handler
  - Enforces owner/PIN gate, slash commands, per-chat queueing, file intake, Gemini calls.
- Gemini runner
  - `headless`: executes `gemini -p ... --output-format json`, resumes sessions with `--resume`.
  - `acp`: launches `gemini --experimental-acp`, keeps one long-lived transport, and streams session updates.
- Hook bootstrap
  - Generates runtime Gemini system settings and injects `BeforeTool` safety hook with `GEMINI_CLI_SYSTEM_SETTINGS_PATH`.
- State store
  - Persists owner binding, chat->session mapping, PIN metadata.
- Interaction logger
  - Writes JSONL records and human-readable terminal logs for inbound/outbound/tool activity.

### Startup sequence

```text
portal
  |
  v
Load global config (~/.portal/config.json) + env overrides
  |
  v
Resolve project runtime paths (./.portal/*)
  |
  v
Load state.json -> PIN metadata / owner/session state
  |
  v
Generate Gemini hook settings (gemini-system-settings.portal.json)
  |
  v
Start HTTP health server (port fallback if in use)
  |
  v
Register Telegram commands + start poller
  |
  v
Ready (PIN shown, diagnostics streaming)
```

### Startup flow

1. `portal` optionally opens an interactive launcher UI (TTY only; can be disabled with `--no-ui`).
2. `portal` resolves config from `~/.portal/config.json` + env overrides.
3. Runtime paths are prepared in project-local `./.portal/`.
4. PIN metadata is loaded; PIN is reused if still valid (30 min TTL).
5. Gemini hook settings file is generated/updated.
6. Telegram identity is checked (`getMe`), commands are registered.
7. HTTP server starts on requested port or next available port.
8. Poller starts, bot begins processing Telegram updates.

### Message lifecycle

1. Telegram update arrives through poller.
2. Handler validates owner and PIN gate (`/connect <PIN>` if not connected).
3. Handler applies per-chat backpressure:
   - 1 running request
   - up to 1 queued request
   - extra messages rejected with wait notice.
4. Optional file intake:
   - document/photo downloaded to `./.portal/agent_sandbox/uploads`
   - prompt is augmented with local file path.
5. Prompt is wrapped with Portal system envelope (regular mode) and sent to Gemini CLI.
6. Session ID from Gemini response/session is persisted for resume on next prompt.
7. Markdown-like output is converted to Telegram HTML for proper client formatting.
8. If response includes `<portal_send_file>...</portal_send_file>`, Portal uploads files/photos back to Telegram.

### Request lifecycle (single message)

```text
Telegram message/photo/doc
  |
  v
Handler: auth + PIN + owner check
  |
  v
Per-chat queue gate (1 running, 1 queued)
  |
  v
Optional file download -> ./agent_sandbox/uploads
  |
  v
Build Gemini prompt (system envelope or assistant mode)
  |
  v
Run Gemini backend:
  - headless: gemini -p ... --output-format json [--resume <session>]
  - acp: persistent ACP session prompt over --experimental-acp transport
  |
  v
Parse result/session_id
  |
  +--> parse <portal_send_file> directives --> upload photo/document
  |
  v
Format response for Telegram HTML and send
  |
  v
Persist updated session/state/logs
```

### Session and state model

- Chat session continuity:
  - Each Telegram chat keeps its own Gemini `session_id`.
  - `/newsession` clears current chat session only.
- PIN and owner continuity:
  - PIN metadata (issued/expiry/failed attempts) is stored.
  - If server restarts within PIN TTL and owner matches, connection is auto-restored.
- Owner binding:
  - First successful `/connect` binds owner user/chat if not already set.

### Safety layers

- User input guard:
  - Destructive command patterns are blocked before Gemini execution.
- Gemini tool hook guard:
  - Runtime `BeforeTool` hook blocks destructive `run_shell_command` calls.
- Assistant mode controls:
  - Requires `GEMINI_API_KEY`.
  - Requires Gemini auth selected type `gemini-api-key`.
  - Forces read-only policy + assistant system prompt.
- Access controls:
  - Owner-only operation after binding.
  - 3 failed PIN attempts trigger shutdown.

### Safety layering

```text
User request
  |
  +--> Portal text guard (destructive intent patterns)
  |
  +--> Gemini execution
         |
         +--> Injected BeforeTool hook (blocks destructive run_shell_command)
         |
         +--> Assistant mode policy (read-only constraints when enabled)
```

### Runtime files

- Global config:
  - `~/.portal/config.json` (telegram credentials, defaults, and last startup profile)
- Project runtime directory:
  - `./.portal/state.json`
  - `./.portal/logs/interactions.jsonl`
  - `./.portal/logs/gemini-hooks.jsonl`
  - `./.portal/gemini-system-settings.portal.json`
  - `./.portal/agent_sandbox/`

## Environment overrides

- `PORTAL_MACHINE_ID=my-laptop` (optional stable ID shown in status/conflict alerts)
- `PORTAL_MODE=regular|assistant`
- `PORTAL_GEMINI_BACKEND=headless|acp` (default `headless`)
- `PORTAL_ACP_LIVE_TRACE=1|0` (default `1`, only used when backend is `acp`)
- `PORTAL_GEMINI_HOOKS_ENABLED=1|0` (default `1`)
- `PORTAL_GEMINI_HOOKS_SETTINGS_PATH=.portal/gemini-system-settings.portal.json`
- `PORTAL_GEMINI_HOOKS_LOG_FILE=.portal/logs/gemini-hooks.jsonl`
- `PORTAL_FILE_UPLOADS_ENABLED=1|0`
- `PORTAL_FILE_MAX_BYTES=10485760`
- `PORTAL_FILE_ALLOWED_EXTENSIONS=.txt,.md,...` (applies to non-image files)
- `PORTAL_FILE_UPLOADS_DIR=.portal/agent_sandbox/uploads`