An autonomous AI agent with real tools, persistent memory, multi-channel presence, and the ability to modify and restart itself.
ForkScout is not a chatbot wrapper. It is a fully autonomous agent that runs as a long-lived process on your server, connects to Telegram (and optionally a terminal), executes real shell commands, reads and writes files, browses the web, searches the internet, manages its own codebase, and remembers everything across sessions. It has no hard-coded restrictions — it reasons, decides, and acts from its own judgment.
- Bun v1.0+ —
curl -fsSL https://bun.sh/install | bash - Docker + Docker Compose — for SearXNG and memory MCP
- A Telegram bot token from @BotFather
- At least one LLM API key (OpenRouter is recommended — one key, access to all models)
git clone https://github.com/martianacademy/forkscout
cd forkscout-agent
bun install# Minimum required
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
# LLM provider — set the key for whichever provider you use (OpenRouter recommended)
OPENROUTER_API_KEY=your_openrouter_key
# Optional — only needed if switching to that provider
ANTHROPIC_API_KEY=
GOOGLE_GENERATIVE_AI_API_KEY=
XAI_API_KEY=
REPLICATE_API_TOKEN=
HUGGING_FACE_API_KEY=
DEEPSEEK_API_KEY=
PERPLEXITY_API_KEY=
ELEVENLABS_API_KEY=Create .forkscout/auth.json. Find your Telegram user ID by messaging @userinfobot:
mkdir -p .forkscout
cat > .forkscout/auth.json <<'EOF'
{
"telegram": {
"ownerUserIds": [YOUR_TELEGRAM_USER_ID]
}
}
EOFLeave the file absent or both lists empty to run in dev mode — every user gets owner access. Safe for local development.
docker-compose up -dWhat this starts:
| Service | Port | Purpose |
|---|---|---|
| SearXNG | 8080 |
Self-hosted private search engine — used by the web_search tool |
| forkscout-memory-mcp | 3211 |
Persistent memory MCP server — stores facts, entities, exchanges across sessions |
Both are optional. Without SearXNG, web_search will fail (configure an alternative search URL in the tool if needed). Without the memory MCP, the agent still works but has no long-term memory.
Verify services are up:
# SearXNG
curl -s http://localhost:8080/search?q=test&format=json | jq '.results | length'
# Memory MCP
curl -s http://localhost:3211/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}' | jq '.result.tools | length'# Telegram bot (production)
bun start
# Telegram bot with hot reload (development)
bun run dev
# Terminal / CLI channel (interactive)
bun run cli
# Terminal with hot reload
bun run cli:devSend your bot a message on Telegram:
/start
It should respond with a greeting. Then try:
What time is it?
Run `uname -a` and tell me the OS.
Search the web for "Vercel AI SDK v6 release notes".
Read src/config.ts and explain what it does.
bun run typecheck
# Expected: no output, exit code 0bun run devtools
# Opens DevTools UI at http://localhost:4983
# Visualises LLM requests, tool calls, token usage in real time| Port | Service | Config location |
|---|---|---|
8080 |
SearXNG (web search) | docker-compose.yml |
3211 |
forkscout-memory-mcp | docker-compose.yml + src/mcp-servers/forkscout_memory.json |
4983 |
AI SDK DevTools | Fixed by @ai-sdk/devtools |
# Stop the bot process
bun run stop
# Stop Docker services
docker-compose down
# Stop Docker services and delete data volumes
docker-compose down -v- What ForkScout Is
- Architecture Overview
- Features
- Channels
- Tools
- Task Orchestration
- Proactive Telegram Messaging
- MCP Servers
- LLM Providers
- Configuration
- Auth & Access Control
- Telegram Commands
- Chat History & Memory
- Token Pipeline & Auto-Compression
- Logging & Activity Log
- Self-Restart & Blue-Green Deploy
- Self-Repair Protocol
- Project Structure
- Getting Started
- Run with Docker
- Environment Variables
- Scripts
- Real-World Use Cases
- Adding a New Tool
- Adding a New LLM Provider
- Adding an MCP Server
- AI SDK v6 Rules
- Roadmap
ForkScout is a self-hosted, autonomous AI agent built on:
- Bun — fast TypeScript runtime, no Node.js required
- Vercel AI SDK v6 — LLM abstraction, tool calling, streaming
- TypeScript strict mode — fully typed throughout
- MCP (Model Context Protocol) — plug external capabilities via JSON config, zero code
It is designed around several core principles:
- Tool-native — the agent uses real tools (shell, filesystem, web, search, memory) rather than generating text answers. Every capability is a callable function.
- Provider-agnostic — swap the LLM (OpenRouter, Anthropic, Google, xAI, DeepSeek, Perplexity, Replicate, HuggingFace, Vercel) by changing one JSON field. No code changes.
- Config-driven — identity, model selection, rate limits, auth, token budgets — everything lives in
src/forkscout.config.jsonand an optional gitignored.forkscout/auth.json. - MCP-first — external capabilities (memory, GitHub, documentation search, sequential thinking) are connected via MCP servers. Drop a JSON file → tool is live.
- Persistent memory — the agent remembers conversations and facts across restarts via a dedicated memory MCP server.
- Self-modifying — the agent can edit its own source code, verify it compiles, and restart itself from Telegram.
- Multi-channel — same agent brain runs on Telegram, terminal, and (planned) voice and web.
src/index.ts ← entry point, picks channel by argv
src/config.ts ← loads forkscout.config.json + .forkscout/auth.json
src/forkscout.config.json ← all runtime config
src/channels/ ← user-facing interfaces
types.ts ← Channel interface
chat-store.ts ← disk-backed per-chat history (shared by all channels)
telegram/
index.ts ← long-poll bot, auth, commands, history, queue
api.ts ← raw Telegram Bot API calls
format.ts ← markdown→HTML, message splitting, HTML stripping
access-requests.ts ← access request persistence and auth.json writer
terminal/
index.ts ← interactive readline CLI with live token streaming
src/agent/ ← LLM runner
index.ts ← runAgent(), streamAgent(), buildAgentParams(), wrapToolsWithAutoCompress()
system-prompts/
identity.ts ← buildIdentity(config) → full system prompt
src/providers/ ← LLM provider registry
index.ts ← getProvider(name), getModel(config)
open_ai_compatible_provider.ts
openrouter_provider.ts
anthropic_provider.ts
google_provider.ts
xai_provider.ts
vercel_provider.ts
replicate_provider.ts
huggingface_provider.ts
deepseek_provider.ts
perplexity_provider.ts
elevenlabs_provider.ts ← TTS + STT only, not in LLM registry
src/tools/ ← auto-discovered LLM-callable tools
auto_discover_tools.ts ← scans *.ts, collects bootstrapTools + allTools
browse_web.ts
compress_text.ts
list_dir.ts
read_file.ts
read_folder_standards.ts
run_shell_commands.ts
think_step_by_step.ts
web_search.ts
write_file.ts
src/mcp-servers/ ← auto-discovered MCP server configs
auto_discover_mcp.ts ← scans *.json, connects enabled servers
forkscout_memory.json ← persistent memory MCP
context7.json ← library documentation lookup
deepwiki.json ← GitHub repository documentation
sequential_thinking.json ← structured multi-step reasoning
src/logs/
logger.ts ← tagged per-module logger (info/warn/error)
activity-log.ts ← NDJSON event log at .forkscout/activity.log
src/llm/
summarize.ts ← llmSummarize() — fast-tier LLM synthesis with extractive fallback
src/utils/
extractive-summary.ts ← extractiveSummary(), compressIfLong() — TF-scored, no LLM
- Multi-step reasoning — agent loops through tool calls until the task is done (configurable max steps)
- Bootstrap tools —
think_step_by_stepandrun_shell_commandsare injected at step 0 before any user message, ensuring the agent always has these available first - Tool exclusion by role — shell and write tools can be restricted to owners only
- Auto-compression pipeline — every tool result is automatically compressed before entering LLM context (≤400 words: pass-through; 400–2000: extractive; >2000: LLM synthesis on fast tier)
- Streaming — terminal channel streams tokens live; Telegram sends typing indicator every 4s
- Configurable identity — agent name, github, description, and extra system prompt instructions from JSON
- Self HTTP server — embedded HTTP server on configurable port (default
3200) accepts trigger requests to spawn new agent self-sessions chain_of_workers— sequential self-session chain where each step's output feeds the next. Agent writes a todo file, fires the next session, current session ends cleanly. Each step can optionally post a🔄 Step startednotification to Telegramparallel_workers— dispatches N independent worker self-sessions concurrently. Each worker writes results to.forkscout/tasks/{batch}/and flips its plan.md checkbox when done- Live progress card — pure-JS monitor (zero LLM cost while waiting) updates a single Telegram message every 3 seconds showing
[ ]/[x]status per worker. Auto-fires the aggregator session when all tasks complete - Aggregator session — when all workers finish, a final self-session compiles results, sends summary to user via Telegram, and cleans up task files
- Confirmation gate — on human channels (Telegram/terminal), agent always presents the full execution plan (workers, tasks, aggregator action) and waits for explicit user confirmation before firing. Self-sessions skip this gate
list_active_workers— inspect all active batches: per-worker status, progress fraction (e.g.3/5), which batches have a live monitor- Monitor state persistence — monitor state is saved to
.forkscout/monitors/{batch}.json. Survives Bun restarts - Orphan recovery on restart — on startup, agent detects orphaned monitors from previous run, sends a detailed Telegram notification (progress, per-task status, started timestamp) to all owners. Does NOT auto-resume — user must explicitly confirm
manage_workers— resume, cancel, or delete an orphaned batch after restart.resumerestarts the progress card;cancelstops monitor keeping files;deleteremoves everything including task files
- Telegram — full-featured bot with auth, history, queuing, rate limiting, owner commands
- Terminal — interactive CLI with live token streaming, same agent brain
- Voice (planned) — ElevenLabs TTS+STT
- Web (planned) — HTTP SSE endpoint for browser frontends
- Role-based:
owner(full access) |user(agent only) |denied - Dev mode: both lists empty → everyone gets owner (safe for local dev)
- Allowlist managed via Telegram commands — no restart required
- Requests persisted to
.forkscout/access-requests.jsonwith status tracking - Role-aware approvals:
/allow <id> adminor/allow <id>(default: user)
- Per-chat conversation history persisted to
.forkscout/chats/<channel>-<id>.json - Token budget trimming (oldest messages dropped first when budget exceeded)
- Per-tool-result token cap with extractive summarisation (not blind truncation)
- Long-term memory via forkscout-memory MCP — facts, entities, relationships, exchanges
- Agent can read, edit, and write its own source files via
read_file,write_file,run_shell_commands read_folder_standardstool — readsai_agent_must_readme.mdfor anysrc/folder before editing/restartcommand triggers blue-green restart with typecheck + startup health verification
- All events (messages, tool calls, tool results, errors, tokens) logged to
.forkscout/activity.logas NDJSON - Tagged per-module console output (
[telegram],[agent],[tools], etc.) - Queryable with
jqfor debugging
The production channel. Runs as a long-poll Telegram bot.
Flow per message:
- Receive update from Telegram getUpdates API
- Check
/startcommand (always allowed) - Evaluate role (owner / user / denied) against
ownerUserIds+allowedUserIds - Denied users: save access request, notify owners, return status message
- Input length cap check
- Rate limit check (owners exempt)
- Owner commands routed to
handleOwnerCommand - Message queued per
chatId— sequential processing, no race conditions runAgent()called with history, excluded tools, meta context- Response rendered as HTML, split at 4096 chars, sent to Telegram
Key design choices:
- Per-chat
Map<chatId, Promise<void>>queue ensures messages from the same chat never run concurrently runtimeAllowedUsersandruntimeOwnerUsersSets updated immediately on/allow— no restart needed- Chat history compressed before save:
capToolResults→trimHistory
Development and power-user channel. Start with bun run cli.
- Reads from
process.stdinvia readline - Streams tokens live using
streamAgent()→process.stdout.write(chunk) - Same history persistence as Telegram (
chat-store.ts) - Session key:
terminal-<username>
Tools are auto-discovered — drop a .ts file into src/tools/ and it's live on the next restart. No registration, no imports to update.
Silent internal reasoning. Agent calls this before complex tasks to reason step by step. Returns the thought as context. Never shown to users.
Execute any shell command. Returns stdout + stderr + exit code. Timeout configurable. This is how the agent:
- Runs
bun run typecheck - Reads logs
- Installs packages
- Runs git commands
- Checks system state
Owner-only by default (configured in ownerOnlyTools).
Read a file in chunks. Parameters:
path— absolute or relativestartLine(optional) — 1-based, defaults to 1endLine(optional) — defaults to min(200, totalLines)
Returns { content, startLine, endLine, totalLines, hasMore }. The agent is instructed to paginate — never read an entire large file at once.
Write content to a file (creates directories as needed). Owner-only by default.
List directory contents recursively with file sizes.
Search the internet using SearXNG (self-hosted, configured in docker-compose.yml) or any search API. Returns titles, URLs, and snippets.
Fetch and extract the text content of any URL. Uses User-Agent: <agent-name>/<github>. Handles redirects, extracts readable text from HTML.
Compress long text. Two modes:
mode: "extractive"(default) — TF-scored sentence extraction, instant, free, no LLMmode: "llm"— fast-tier LLM synthesis, higher quality, uses tokens
Parameters: text, mode, maxSentences (extractive), maxTokens (LLM), instruction (custom LLM prompt).
Reads src/<folder>/ai_agent_must_readme.md before the agent modifies any folder. Returns the full standards document. Agent is instructed to call this before editing any src/ subfolder.
ForkScout can spawn independent self-sessions to run work in parallel or sequentially — long tasks that would time out in a single turn, or multiple independent analyses running concurrently.
Fire a sequential self-session chain. The next session receives full shared history. Pattern:
1. Write .forkscout/tasks/{name}/todo.md with all steps
2. chain_of_workers({ prompt: "Read todo.md, do step 1, mark done, call chain_of_workers for step 2", chat_id: <id> })
3. Current session ends
4. Next session reads todo.md, does one step, marks done, calls chain again
5. Repeat until all steps ✅ — last session notifies user via telegram_message_tools
Optional chat_id sends 🔄 Step started: "..." to Telegram at each step.
Dispatch N concurrent independent worker self-sessions:
parallel_workers({
batch_name: "analyse-codebase",
tasks: [
{ session_key: "task-auth", label: "Analyse auth", prompt: "...fully self-contained..." },
{ session_key: "task-db", label: "Analyse DB", prompt: "...fully self-contained..." },
],
aggregator_prompt: "Read results, compile summary, send via telegram_message_tools, delete .forkscout/tasks/analyse-codebase/",
chat_id: <user_chat_id>,
})
Each worker:
- Writes results to
.forkscout/tasks/{batch}/{session_key}-result.md - Flips
- [ ] \{session_key}`→- [x]inplan.md` when done
Progress monitor — pure JS, no LLM calls, updates a single Telegram message every 3s. Aggregator fires automatically when all tasks are [x].
Confirmation gate — before firing any workers on a human channel, agent presents the full plan and waits for explicit confirmation ("yes", "karo", "go ahead", etc.).
Lists all batch directories in .forkscout/tasks/. Shows per-worker [ ]/[x] status, progress fraction, and which batches have a live monitor.
Recover after a Bun restart. On startup, orphaned monitors are detected and owners are notified via Telegram with full details. User then explicitly calls:
| Action | Effect |
|---|---|
resume |
Restart monitor from saved state, send fresh progress card |
cancel |
Stop monitor, delete state — task files kept |
delete |
Stop monitor, delete state + entire .forkscout/tasks/{batch}/ |
The telegram_message_tools tool lets the agent reach users without waiting for them to send a message first. Used by cron jobs, background workers, aggregators, and any self-session that needs to notify the user.
| Action | What it sends | Required fields | Limits |
|---|---|---|---|
send |
Text/Markdown | chat_id, text |
4096 chars (auto-split) |
send_to_owners |
Text to all owners | text |
4096 chars (auto-split) |
send_photo |
Image | file_path_or_url |
10 MB upload / 5 MB URL |
send_document |
Any file (PDF, ZIP, CSV, JSON…) | file_path_or_url |
50 MB |
send_voice |
Voice message (OGG/Opus) | file_path_or_url |
50 MB |
send_audio |
Music player card (MP3/M4A) | file_path_or_url |
50 MB |
send_video |
Video (MP4) | file_path_or_url |
50 MB |
send_animation |
GIF / silent MP4 | file_path_or_url |
50 MB |
send_location |
Map pin | latitude, longitude |
— |
send_poll |
Interactive poll | poll_question, poll_options |
2–10 options |
file_path_or_url— absolute local path or public HTTPS URLcaption— optional Markdown caption for all media actions- Media sent to
chat_idif provided, otherwise broadcast to allownerUserIds - Sent messages are saved to recipient's chat history — next turn the agent knows what it already sent
MCP servers are auto-discovered — drop a .json file into src/mcp-servers/ and it connects on next startup. Set "enabled": false to disable without deleting.
Tool names follow the pattern: <server_name>__<tool_name>
Persistent memory across sessions. Tools include:
forkscout-mem__save_knowledge— store a factforkscout-mem__search_knowledge— semantic search over stored factsforkscout-mem__add_entity/add_relation— knowledge graphforkscout-mem__add_exchange— record conversation for later recallforkscout-mem__search_entities/search_exchangesforkscout-mem__start_task/complete_task/check_tasks— task trackingforkscout-mem__get_self_entity— agent's own identity/beliefsforkscout-mem__self_observe— record a behavioral observation
URL: http://localhost:3211/mcp
Fetches up-to-date documentation for any library. The agent uses this to look up exact API signatures before writing code — no hallucinated APIs.
Tools: resolve-library-id, get-library-docs
AI-powered documentation for any GitHub repository. Ask questions about codebases.
Tools: read_wiki_structure, read_wiki_contents, ask_question
Structured multi-step reasoning with branching and revision. Useful for complex problem decomposition.
Tools: sequentialthinking
All providers implement OpenAICompatibleProvider { name: string; chat(modelId): LanguageModel }.
Switch provider and tier by editing two fields in src/forkscout.config.json:
"llm": {
"provider": "openrouter",
"tier": "balanced"
}No code changes. No restart of code — just restart the process.
| Provider | Key | Fast | Balanced | Powerful |
|---|---|---|---|---|
| OpenRouter | OPENROUTER_API_KEY |
gemini-2.0-flash-001 | minimax-m2.5 | claude-sonnet-4-5 |
| Anthropic | ANTHROPIC_API_KEY |
claude-haiku-4-5 | claude-sonnet-4-5 | claude-opus-4-5 |
GOOGLE_GENERATIVE_AI_API_KEY |
gemini-2.0-flash | gemini-2.5-pro | gemini-2.5-pro | |
| xAI | XAI_API_KEY |
grok-3-mini-fast | grok-3 | grok-3 |
| Vercel | — | gpt-4o-mini | gpt-4o | claude-sonnet-4-5 |
| Replicate | REPLICATE_API_TOKEN |
llama-3-8b | llama-3.1-405b | llama-3.1-405b |
| HuggingFace | HUGGING_FACE_API_KEY |
llama-3.2-3b | llama-3.3-70b | llama-3.3-70b |
| DeepSeek | DEEPSEEK_API_KEY |
deepseek-chat | deepseek-chat | deepseek-reasoner |
| Perplexity | PERPLEXITY_API_KEY |
sonar | sonar-pro | sonar-pro |
ElevenLabs (ELEVENLABS_API_KEY) is also available for TTS (eleven_flash_v2_5) and STT (scribe_v1) — not in the LLM registry, called directly for voice features.
- Always use
.chat(modelId)notprovider(modelId)— the latter hits the Responses API and breaks non-OpenAI endpoints - Replicate uses
.languageModel(modelId)not.chat()— different SDK shape - Tool definitions use
inputSchema:notparameters:(v6 rename) execute: async (input) => {}— never destructure in signature
All configuration lives in src/forkscout.config.json. Secrets and per-deployment overrides live in .forkscout/auth.json (gitignored). The two files are deep-merged at startup — auth.json wins on conflicts.
{
"telegram": {
"pollingTimeout": 30,
"historyTokenBudget": 12000,
"ownerUserIds": [],
"allowedUserIds": [],
"rateLimitPerMinute": 20,
"maxInputLength": 2000,
"ownerOnlyTools": ["run_shell_commands", "write_file"],
"maxToolResultTokens": 3000,
"maxSentencesPerToolResult": 20
},
"terminal": {
"historyTokenBudget": 12000
},
"agent": {
"name": "ForkScout",
"description": "...",
"github": "https://github.com/martianacademy/forkscout",
"systemPromptExtra": "Optional extra instructions appended to identity"
},
"llm": {
"provider": "openrouter",
"tier": "balanced",
"maxTokens": 2048,
"maxSteps": 20,
"llmSummarizeMaxTokens": 1200,
"toolResultAutoCompressWords": 400,
"providers": { ... }
}
}| Field | Default | Description |
|---|---|---|
telegram.pollingTimeout |
30 | Long-poll timeout in seconds |
telegram.historyTokenBudget |
12000 | Max tokens in per-chat history before trimming oldest messages |
telegram.ownerUserIds |
[] |
Telegram user IDs with full access. Empty = dev mode (everyone is owner) |
telegram.allowedUserIds |
[] |
Telegram user IDs with agent-only access |
telegram.rateLimitPerMinute |
20 | Max messages per user per minute. 0 = disabled. Owners exempt. |
telegram.maxInputLength |
2000 | Max characters per message. 0 = disabled. |
telegram.ownerOnlyTools |
["run_shell_commands", "write_file"] |
Tools blocked for non-owners |
telegram.maxToolResultTokens |
3000 | Per-result token cap in history (uses extractive summarisation) |
telegram.maxSentencesPerToolResult |
20 | Max sentences when extractively compressing a tool result |
terminal.historyTokenBudget |
12000 | Same as telegram, for terminal sessions |
llm.provider |
"openrouter" |
Active LLM provider |
llm.tier |
"balanced" |
"fast" / "balanced" / "powerful" |
llm.maxTokens |
2048 | Max output tokens per LLM call |
llm.maxSteps |
20 | Max tool-call steps per agent turn |
llm.llmSummarizeMaxTokens |
1200 | Max output tokens for LLM summarisation calls |
llm.toolResultAutoCompressWords |
400 | Word count threshold for auto-compression pipeline |
agent.name |
"ForkScout" |
Agent display name (used in prompts, headers, Telegram greeting) |
agent.github |
— | GitHub URL (used in HTTP-Referer headers, identity) |
agent.systemPromptExtra |
— | Optional extra text appended after the base identity prompt |
| Role | Access | Who |
|---|---|---|
owner |
Everything — shell, write, all tools, all commands | ownerUserIds in config/auth.json |
user |
Agent chat only, no shell/write tools | allowedUserIds in config/auth.json |
denied |
Nothing — access request flow | Everyone else |
Dev mode: if both ownerUserIds and allowedUserIds are empty arrays, every user gets owner access. Safe for local development.
- Unknown user sends any message
- Bot responds: "⛔ You're not on the allowlist. Your request has been sent to the admin."
- All owners receive a notification with name, userId, chatId, username
- Request saved to
.forkscout/access-requests.jsonwithstatus: "pending" - Owner uses
/allow <userId>or/allow <userId> adminor/deny <userId> - On approval: user added to
runtimeAllowedUsers(immediate) +auth.json(persists restart) - User notified of approval/denial
If user messages again while pending: "⏳ Still pending review." If user messages after denial: "⛔ Your request was denied."
[
{
"userId": 123456789,
"chatId": 123456789,
"username": "johndoe",
"firstName": "John",
"requestedAt": "2026-02-25T10:00:00.000Z",
"status": "approved",
"role": "user",
"reviewedAt": "2026-02-25T10:05:00.000Z",
"reviewedBy": 987654321
}
]All commands work for owners only (except /start which is open to all). Commands are registered in Telegram's autocomplete menu at startup, scoped per owner chat — other users see no command list until explicitly opened up.
| Command | Description |
|---|---|
/start |
Greeting message. Available to all users before authentication. |
/whoami |
Shows your Telegram user ID, chat ID, and confirms your current role (owner / admin / user). |
| Command | Description |
|---|---|
/allow <userId> |
Approve a pending access request and grant the user user role. |
/allow <userId> admin |
Approve a pending access request and grant the user admin (owner) role. |
/deny <userId> |
Reject a pending access request. |
/pending |
List all users with a pending access request, including their username. |
/requests |
List all access requests with their current status and assigned role. |
| Command | Description |
|---|---|
/restart |
Trigger a blue-green restart: runs typecheck, starts new instance, health-checks, then kills old. |
| Command | Description |
|---|---|
/secret store <alias> <value> |
Encrypt and store a secret under the given alias. The message is immediately deleted from Telegram. |
/secret list |
List all stored secret aliases (values are never shown). |
/secret delete <alias> |
Delete a stored secret by alias. |
/secret env <VAR_NAME> [alias] |
Import an environment variable from the server into the vault. Value never passes through Telegram. |
/secret sync |
Import all variables from the server's .env file into the vault at once. |
/secret help |
Show all /secret subcommands and usage. |
Secrets are stored AES-256-GCM encrypted in .forkscout/vault.enc.json (gitignored). The agent uses {{secret:alias}} placeholders in tool calls — raw values are never passed through Telegram or visible in logs.
Every conversation is persisted to disk:
- Telegram:
.forkscout/chats/telegram-<chatId>.json - Terminal:
.forkscout/chats/terminal-<username>.json
Format: ModelMessage[] (Vercel AI SDK v6 format). Loaded on first message per session, cached in memory, saved after every turn.
History pipeline (each turn):
- Load from disk (if not in memory cache)
capToolResults()— compress oversized tool results using extractive summarisationtrimHistory()— drop oldest messages until underhistoryTokenBudgettokens- Pass to
runAgent()aschatHistory - Append
result.responseMessagesto history capToolResults()again on the combined historytrimHistory()again- Save to disk
Every ModelMessage content is properly serialised before counting:
textparts:encode(text).lengthtool-callparts:encode(JSON.stringify(input)).lengthtool-resultparts:encode(JSON.stringify(output)).length— AI SDK v6 usesoutputfield (notresult)- Other parts (images, files): flat 512-token estimate
Via forkscout_memory MCP server. The agent stores:
- Conversation facts: names, preferences, decisions made
- Engineering knowledge: patterns, gotchas, fixes applied
- Entities and relationships: people, projects, technologies
- Task state: active work items across sessions
The agent can recall past context from memory as naturally as consulting history.
Every tool result goes through a compression pipeline before entering LLM context:
Tool executes → result returned
↓
wrapToolsWithAutoCompress()
↓
word count check
├─ ≤ 400 words → pass through unchanged
├─ 400–2000 words → extractiveSummary({ maxSentences: 12 })
│ TF-scored sentence extraction, free, instant
└─ > 2000 words → llmSummarize() on fast tier
LLM synthesis, max 1200 tokens
Falls back to extractive on error
↓
compressed result enters agent context
Why this matters: A single browse_web or run_shell_commands result can be 10,000–50,000 tokens. Without compression, one tool call exhausts the entire context window. With compression, the agent can use dozens of tools per session without running out of context.
History compression (separate pipeline, on saved history):
capToolResults()— extractive summarisation on any tool result exceedingmaxToolResultTokens(3000) in the saved historytrimHistory()— drop oldest messages when total history exceedshistoryTokenBudget(12000 tokens)
const logger = log("telegram");
logger.info("Starting long-poll..."); // → [telegram] Starting long-poll...
logger.warn("Rate limit exceeded");
logger.error("Agent error:", err);Every module creates its own tagged logger. Output is human-readable in terminal.
All events written to .forkscout/activity.log as NDJSON (one JSON object per line).
Event types:
msg_in— incoming message from usermsg_out— agent response senttool_call— tool invoked with inputtool_result— tool result receivedtoken— streaming token chunk (terminal channel)info/warn/error— system events
Each event includes timestamp, channel, chatId, type, and relevant payload.
Querying:
# Last 50 events
tail -50 .forkscout/activity.log | jq .
# All errors
grep '"type":"error"' .forkscout/activity.log | tail -20
# Tool calls and results
grep '"type":"tool_call"\|"type":"tool_result"' .forkscout/activity.log | tail -30
# Messages from a specific chat
grep '"chatId":123456789' .forkscout/activity.log | tail -20Send /restart from Telegram to restart the bot without SSH access.
/restart
│
├─ Step 1: bun run typecheck
│ ❌ fails → send typecheck errors to Telegram, abort. Bot stays up.
│ ✅ passes → "Typecheck passed. Spawning new instance..."
│
├─ Step 2: Bun.spawn new process (detached)
│
├─ Step 3: Wait 6 seconds
│ ❌ new process crashed → send error + trigger self-diagnosis agent run
│ ✅ still alive → "New instance is healthy. Handing off now."
│
└─ Step 4: process.exit(0) (old process hands off to new)
Startup failures (bad API key, broken MCP server, config parse error, missing env var) all manifest within the first few seconds. 6 seconds is enough to catch any startup-time crash while being fast enough to not feel slow.
Between process.exit(0) and the new process picking up polling, there is a brief gap (1–3 seconds) during which Telegram messages queue up server-side. The new process picks them up immediately via getUpdates with offset starting at 0 — no messages are lost because Telegram holds unacknowledged updates.
When /restart fails because the new instance crashed:
-
Bot notifies you: "❌ Restart aborted — new instance crashed at startup (exit N). Current bot still running. 🔍 Asking the agent to self-diagnose..."
-
A new agent run is fired automatically with this task:
SYSTEM: Self-restart just failed. The new instance crashed at startup with exit code N. The current process is still running. Your job: 1. Check recent logs: tail -50 .forkscout/activity.log 2. Check for startup errors: bun run src/index.ts 2>&1 | head -40 3. Identify the root cause 4. Fix it 5. Run bun run typecheck to verify 6. Send /restart to try again -
The agent reads logs, boots the process briefly to capture the crash, identifies root cause, applies a fix, typechecks, and issues
/restartitself. -
If the problem requires your input (e.g. missing API key), the agent tells you exactly what's needed.
forkscout-agent/
├── src/
│ ├── forkscout.config.json ← all runtime config
│ ├── config.ts ← config loader + types
│ ├── index.ts ← entry point
│ ├── agent/
│ │ ├── index.ts ← runAgent, streamAgent, wrapToolsWithAutoCompress
│ │ └── system-prompts/
│ │ └── identity.ts ← buildIdentity(config)
│ ├── channels/
│ │ ├── types.ts ← Channel interface
│ │ ├── chat-store.ts ← disk-backed history
│ │ ├── telegram/
│ │ │ ├── index.ts ← bot logic
│ │ │ ├── api.ts ← Telegram API client
│ │ │ ├── format.ts ← message formatting
│ │ │ └── access-requests.ts ← auth request persistence
│ │ └── terminal/
│ │ └── index.ts ← CLI channel
│ ├── providers/ ← LLM provider registry (9 providers)
│ ├── tools/ ← auto-discovered tools (9 tools)
│ ├── mcp-servers/ ← auto-discovered MCP configs (4 servers)
│ ├── llm/
│ │ └── summarize.ts ← llmSummarize()
│ ├── logs/
│ │ ├── logger.ts ← tagged logger
│ │ └── activity-log.ts ← NDJSON event log
│ └── utils/
│ └── extractive-summary.ts ← extractiveSummary(), compressIfLong()
├── .forkscout/ ← runtime data (gitignored)
│ ├── auth.json ← owner/allowed user IDs (secrets)
│ ├── access-requests.json ← Telegram access request history
│ ├── activity.log ← NDJSON event log
│ └── chats/ ← per-chat conversation history
├── docker-compose.yml ← SearXNG + memory MCP
├── Dockerfile
├── package.json
├── tsconfig.json
└── README.md
- Bun v1.0+
- A Telegram bot token from @BotFather
- An LLM API key (OpenRouter recommended — one key, access to all providers)
git clone https://github.com/martianacademy/forkscout
cd forkscout-agent
bun installTELEGRAM_BOT_TOKEN=your_bot_token_here
OPENROUTER_API_KEY=your_openrouter_key_hereCreate .forkscout/auth.json (find your Telegram userId by messaging @userinfobot):
{
"telegram": {
"ownerUserIds": [YOUR_TELEGRAM_USER_ID]
}
}Or leave it empty to run in dev mode (everyone gets owner access — fine for local use).
docker-compose up -dThis starts:
- SearXNG on port 8080 — private self-hosted search for
web_searchtool - forkscout-memory-mcp on port 3211 — persistent memory
bun start # production (Telegram)
bun run cli # terminal channelSend your bot a message on Telegram. Try:
- "What's the current time and date?"
- "Search the web for the latest news about AI agents"
- "Read the file src/config.ts and explain what it does"
- "Run
ls -laand tell me what's in this directory"
A pre-built image is published to GitHub Container Registry on every release. No Bun, no bun install required.
docker pull ghcr.io/martianacademy/forkscout:latestdocker run -d \
--name forkscout \
--restart unless-stopped \
--env-file .env \
-v $(pwd)/.forkscout:/app/.forkscout \
ghcr.io/martianacademy/forkscout:latest| Flag | Purpose |
|---|---|
--env-file .env |
Injects TELEGRAM_BOT_TOKEN, LLM keys, etc. |
-v $(pwd)/.forkscout:/app/.forkscout |
Persists auth, chat history, and activity log across restarts |
--restart unless-stopped |
Auto-restarts on crash or reboot |
# Create .env first, then:
docker-compose up -dThis starts all three services together: the agent, SearXNG (port 8080), and forkscout-memory-mcp (port 3211).
| Tag | Description |
|---|---|
latest |
Latest stable release |
v3.0.0 |
Pinned version |
Registry: ghcr.io/martianacademy/forkscout
Package page: https://github.com/martianacademy/forkscout/pkgs/container/forkscout
docker logs -f forkscout
# Or read the structured activity log
docker exec forkscout tail -50 /app/.forkscout/activity.log# Stop
docker stop forkscout && docker rm forkscout
# Update to latest
docker pull ghcr.io/martianacademy/forkscout:latest
docker stop forkscout && docker rm forkscout
docker run -d --name forkscout --restart unless-stopped \
--env-file .env \
-v $(pwd)/.forkscout:/app/.forkscout \
ghcr.io/martianacademy/forkscout:latest| Variable | Required for | Notes |
|---|---|---|
TELEGRAM_BOT_TOKEN |
Telegram channel | From @BotFather |
OPENROUTER_API_KEY |
OpenRouter provider | Recommended — access to all models |
ANTHROPIC_API_KEY |
Anthropic provider | Direct Anthropic API |
GOOGLE_GENERATIVE_AI_API_KEY |
Google provider | Google AI Studio |
XAI_API_KEY |
xAI provider | Grok models |
REPLICATE_API_TOKEN |
Replicate provider | Open-source models |
HUGGING_FACE_API_KEY |
HuggingFace provider | HF Inference API |
DEEPSEEK_API_KEY |
DeepSeek provider | DeepSeek models |
PERPLEXITY_API_KEY |
Perplexity provider | Sonar models with web search |
ELEVENLABS_API_KEY |
Voice features | TTS + STT |
Only one LLM key is required — whichever provider is set as llm.provider in config.
Before making any changes to files in src/ or system files, always create a checkpoint commit:
# Stage all changes and create a checkpoint commit
git add -A && git commit -m "Checkpoint: <describe current state and what you will change>"Why? This creates a safe restore point. If your changes break the agent, revert with:
git reset --hard <commit-hash>When to checkpoint:
- Before any refactoring
- Before adding new tools, channels, or providers
- Before modifying the agent core logic
- Before upgrading dependencies (AI SDK, Bun, etc.)
After making changes, always:
- Run
bun run typecheck— must pass with no errors - Test with
bun run devorbun start - If working, continue; if broken,
git reset --hardto checkpoint and start over
bun start # kill existing instance, start production (Telegram)
bun run dev # kill existing instance, start with hot reload (Telegram)
bun run cli # kill existing instance, start terminal channel
bun run cli:dev # kill existing instance, start terminal + hot reload
bun run stop # kill all running agent instances
bun run typecheck # tsc --noEmit (0 errors = clean)
bun run devtools # AI SDK DevTools UI at http://localhost:4983bun start and bun run dev always run bun run stop first — safe to call anytime without worrying about duplicate processes.
- Answer questions, do research, summarise documents
- Remember your preferences, ongoing projects, past conversations
- Run tasks while you're away and report back
- Monitor logs: "Check if there are any errors in the last hour"
- Deploy code: "Pull latest from main, run tests, restart the service"
- Disk management: "Find files larger than 1GB and tell me what they are"
- Process monitoring: "Is the API server still running? What's its memory usage?"
- "Read src/agent/index.ts and explain how the token compression works"
- "Check the TypeScript errors and fix them"
- "Browse the Vercel AI SDK docs and tell me how to use streamText"
- Self-modification: "Add a /status command to the Telegram channel"
- "Search for recent papers on LLM agents and summarise the top 5"
- "Browse these 3 URLs and compare their approaches to X"
- "Look up the GitHub repo for Y and tell me how to use the library"
- CRM-style memory: remember customer names, preferences, history
- Scheduled reporting (via shell + cron): "Generate a daily summary of activity"
- Document processing: read files, extract data, write reports
- Control smart home via local API calls
- Monitor and log sensor data
- Alert on anomalies
- Different roles for team members (owner = full shell access, user = chat only)
- Knowledge base: facts stored in memory MCP shared across team queries
- Audit trail: every message and tool call logged
- "Explain this codebase to me, file by file"
- "What changed in the last 10 git commits?"
- "Generate API documentation for all the functions in src/tools/"
- Long-form writing with memory across sessions
- Research + write: "Find information on X and draft a 1000-word article"
- Iterative editing: remembers previous drafts, applies feedback
- Create
src/tools/my_tool.ts:
import { tool } from "ai";
import { z } from "zod";
export const IS_BOOTSTRAP_TOOL = false; // true = injected at step 0
export const my_tool = tool({
description: "One clear sentence: what this tool does and when to use it.",
inputSchema: z.object({
param: z.string().describe("What this param is for")
}),
execute: async (input) => {
try {
// your implementation
return { success: true, result: input.param };
} catch (err: any) {
return { success: false, error: err.message };
}
}
});- Restart the bot. The tool is discovered automatically — no imports, no registration.
Rules:
- File name must be
snake_case.ts - Export name must exactly match the file name (minus
.ts) - One tool per file — auto-discovery only picks one export
- Error returns must be
{ success: false, error: string } - Never destructure in
executesignature — type inference breaks - Use
inputSchema:notparameters:(AI SDK v6)
- Create
src/providers/myprovider_provider.ts:
import { createOpenAI } from "@ai-sdk/openai"; // or appropriate SDK
import type { OpenAICompatibleProvider } from "./open_ai_compatible_provider.ts";
export function createMyProvider(): OpenAICompatibleProvider {
return {
name: "myprovider",
chat(modelId: string) {
return createOpenAI({
baseURL: "https://api.myprovider.com/v1",
apiKey: process.env.MYPROVIDER_API_KEY!
}).chat(modelId);
}
};
}- Register in
src/providers/index.ts:
import { createMyProvider } from "./myprovider_provider.ts";
const registry: Record<string, OpenAICompatibleProvider> = {
// ...existing providers
myprovider: createMyProvider()
};- Add model tiers to
src/forkscout.config.json:
"myprovider": {
"fast": "model-fast",
"balanced": "model-balanced",
"powerful": "model-powerful"
}- Switch to it: set
"provider": "myprovider"in config and restart.
Drop a JSON file into src/mcp-servers/:
stdio server (local process):
{
"name": "my_server",
"enabled": true,
"command": "npx",
"args": ["-y", "@some/mcp-server"]
}SSE server (HTTP):
{
"name": "my_server",
"enabled": true,
"url": "http://localhost:3100/sse"
}With auth headers:
{
"name": "my_server",
"enabled": true,
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer ${MY_API_KEY}"
}
}Environment variables in headers values are expanded automatically. Restart the bot — tools from the server appear as my_server__tool_name.
Critical rules that prevent subtle bugs:
| Rule | Why |
|---|---|
Use .chat(modelId) not provider(modelId) |
v6 provider(id) calls the Responses API — breaks non-OpenAI endpoints |
Import from "ai" not sub-packages |
import { generateText, streamText } from "ai" |
Use inputSchema: not parameters: |
v6 renamed the field |
execute: async (input) => {} — never destructure |
TypeScript inference breaks on destructured tool inputs |
Use stopWhen: stepCountIs(N) not maxSteps: N |
v6 preferred API |
Tool result parts use output not result |
ModelMessage content parts: part.output (breaking change from v5) |
Replicate: use .languageModel(modelId) not .chat() |
Replicate SDK wraps differently |
Local docs are in node_modules/ai/docs/ — check there before guessing or fetching from the web.
- LLM retry with exponential backoff (429 + 5xx, max 3 retries, backoff 1s→30s)
- Error classification — clean user-facing messages instead of raw SDK errors
- Memory auto-bridging — background job saves key facts after each turn
- Test suite (config, providers, tools, integration)
- Voice channel — ElevenLabs TTS+STT over HTTP
- Web channel — HTTP SSE endpoint for browser frontend
- Self HTTP server — embedded trigger endpoint for self-sessions
- Task orchestration —
chain_of_workers,parallel_workers,list_active_workers,manage_workers - Live Telegram progress card — pure-JS monitor, zero LLM cost, auto-fires aggregator
- Monitor state persistence — survives restarts, orphan recovery with confirmation gate
- Proactive Telegram messaging —
telegram_message_toolswith text, photo, document, voice, audio, video, animation, location, poll - Trust & authorization model (admin/user/self roles with full access matrix)
- Emotional state model (energy, mood, curiosity, social need, stress — proper state machine with event-driven transitions and time decay)
- Goals & long-term planning (goal types: life/high/medium/low, milestones, agent-managed via tools)
- Decision engine (weighted scoring: goal priority × energy × curiosity × urgency × social need)
- Scheduler (cron-like self-initiated tasks)
- Instincts (proactive outreach when social need high, learning when curious)
- Adaptive learning from interaction history
- Self-modification with CI/CD pipeline
- Phone/SMS channel
- Social media presence
- Vision (image understanding)
- Persistent cloud deployment with self-monitoring
- Voice/physical interface integration
- Cross-agent collaboration
MIT
Built by Martian Academy. ForkScout is an ongoing experiment in autonomous AI agents — built to understand what it means for a program to have genuine agency, memory, and presence.