@lyalindotcom/ai-portal
v0.2.2
Published
Telegram bridge for running local Gemini CLI sessions from any project directory.
Maintainers
Readme
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+
geminiCLI installed- Gemini auth configured once manually on this machine
- A Telegram bot token
Install
npm i -g @lyalindotcom/ai-portalQuick start
portalIn 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:
- Open chat with your bot
- Send
/start - Send
/connect <PIN>(PIN is shown in terminal) - Send prompts
First run opens interactive setup and stores config in ~/.portal/config.json.
CLI commands
portalportal startportal configportal --changeportal --reset-ownerportal config --reset-ownerportal --port 3001portal --mode regularportal --mode assistantportal --backend headlessportal --backend acpportal --acp-live-traceportal --no-acp-live-traceportal --uiportal --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
--resumefor session continuity.
- Runs one-shot Gemini CLI commands (
acp(experimental)- Runs Gemini CLI in
--experimental-acpmode with a persistent ACP transport. - Streams tool/message events into Portal logs.
- Falls back to a new ACP session if
loadSessionfails. - Optional Telegram live trace stream can be toggled with
--acp-live-trace/--no-acp-live-trace.
- Runs Gemini CLI in
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.extcaption: 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_commandtool calls before execution - logs hook decisions to
./.portal/logs/gemini-hooks.jsonl
- blocks destructive
- 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
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_sandboxMain components
- CLI entrypoint
- Parses args, runs first-time config wizard, starts server in current folder.
- Express health server
- Serves
/healthzfor local diagnostics.
- Serves
- Telegram client + poller
- Long-polls
getUpdates, handles reconnect/timeout recovery, detects conflict.
- Long-polls
- Telegram handler
- Enforces owner/PIN gate, slash commands, per-chat queueing, file intake, Gemini calls.
- Gemini runner
headless: executesgemini -p ... --output-format json, resumes sessions with--resume.acp: launchesgemini --experimental-acp, keeps one long-lived transport, and streams session updates.
- Hook bootstrap
- Generates runtime Gemini system settings and injects
BeforeToolsafety hook withGEMINI_CLI_SYSTEM_SETTINGS_PATH.
- Generates runtime Gemini system settings and injects
- 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
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
portaloptionally opens an interactive launcher UI (TTY only; can be disabled with--no-ui).portalresolves config from~/.portal/config.json+ env overrides.- Runtime paths are prepared in project-local
./.portal/. - PIN metadata is loaded; PIN is reused if still valid (30 min TTL).
- Gemini hook settings file is generated/updated.
- Telegram identity is checked (
getMe), commands are registered. - HTTP server starts on requested port or next available port.
- Poller starts, bot begins processing Telegram updates.
Message lifecycle
- Telegram update arrives through poller.
- Handler validates owner and PIN gate (
/connect <PIN>if not connected). - Handler applies per-chat backpressure:
- 1 running request
- up to 1 queued request
- extra messages rejected with wait notice.
- Optional file intake:
- document/photo downloaded to
./.portal/agent_sandbox/uploads - prompt is augmented with local file path.
- document/photo downloaded to
- Prompt is wrapped with Portal system envelope (regular mode) and sent to Gemini CLI.
- Session ID from Gemini response/session is persisted for resume on next prompt.
- Markdown-like output is converted to Telegram HTML for proper client formatting.
- If response includes
<portal_send_file>...</portal_send_file>, Portal uploads files/photos back to Telegram.
Request lifecycle (single message)
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/logsSession and state model
- Chat session continuity:
- Each Telegram chat keeps its own Gemini
session_id. /newsessionclears current chat session only.
- Each Telegram chat keeps its own Gemini
- 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
/connectbinds owner user/chat if not already set.
- First successful
Safety layers
- User input guard:
- Destructive command patterns are blocked before Gemini execution.
- Gemini tool hook guard:
- Runtime
BeforeToolhook blocks destructiverun_shell_commandcalls.
- Runtime
- Assistant mode controls:
- Requires
GEMINI_API_KEY. - Requires Gemini auth selected type
gemini-api-key. - Forces read-only policy + assistant system prompt.
- Requires
- Access controls:
- Owner-only operation after binding.
- 3 failed PIN attempts trigger shutdown.
Safety layering
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|assistantPORTAL_GEMINI_BACKEND=headless|acp(defaultheadless)PORTAL_ACP_LIVE_TRACE=1|0(default1, only used when backend isacp)PORTAL_GEMINI_HOOKS_ENABLED=1|0(default1)PORTAL_GEMINI_HOOKS_SETTINGS_PATH=.portal/gemini-system-settings.portal.jsonPORTAL_GEMINI_HOOKS_LOG_FILE=.portal/logs/gemini-hooks.jsonlPORTAL_FILE_UPLOADS_ENABLED=1|0PORTAL_FILE_MAX_BYTES=10485760PORTAL_FILE_ALLOWED_EXTENSIONS=.txt,.md,...(applies to non-image files)PORTAL_FILE_UPLOADS_DIR=.portal/agent_sandbox/uploads
