A Telegram bot that talks to a main Claude agent, which can delegate tasks to restricted sub-agents. First sub-agent is a trivial ping-pong test to prove the plumbing works.
Key insight: Claude Code has a native subagent system (.claude/agents/ + built-in Task tool). No custom MCP server needed for agent spawning. This eliminates ~50% of the original codebase.
Claude Pro/Max subscription covers CLI usage natively. The Agent SDK requires a pay-per-token ANTHROPIC_API_KEY — Anthropic blocks OAuth tokens from Claude Pro/Max for external API calls. Process spawning overhead (~1-2s) is acceptable for a personal bot.
Claude Code's built-in Task tool delegates to agents defined in .claude/agents/*.md. Each agent file specifies its name, description, tools, and system prompt. The main agent sees these as available delegation targets automatically. One level of nesting only (subagents can't spawn subagents).
All Claude Code config lives in jarvis/.claude/. The CLI runner passes --setting-sources project to block ~/.claude/ settings, and --system-prompt-file to replace default prompts. Your personal Claude Code setup stays untouched.
Memory is a markdown file injected into the system prompt on every call. Read is free (part of the prompt). Write goes through a skill that uses Bash to append to the file.
Thin bridge. Receives messages from Telegram, passes them to the main agent, sends the response back.
Responsibilities:
- Auth: allowlist of Telegram user IDs from env
/resetcommand: starts fresh session- Typing indicator while waiting for Claude
- Response chunking: split at 4096 chars on paragraph boundaries
- Error handling: surface failures as user-readable messages
No logic, no routing — just plumbing.
Wraps claude -p process spawning. Single entry point: runMainAgent(message, sessionId).
Spawns:
claude -p \
--setting-sources project \
--system-prompt-file agents/main/system-prompt.md \
--append-system-prompt-file agents/main/memory.md \
--resume <session-id> \
--output-format json \
"user message"Key flags:
--setting-sources project— isolates from personal~/.claude/--system-prompt-file— replaces default system prompt entirely--append-system-prompt-file— injects memory as additional context--resume— conversation continuity across messages--output-format json— structured output withsession_idfor first-call capture--tools ""— REMOVED (main agent needs built-in Task tool for subagent delegation)
The main agent gets Claude Code's built-in tools (Read, Write, Bash, Task) plus whatever we allow in .claude/settings.json. The Task tool is how it delegates to subagents.
JSON file mapping Telegram chat ID → Claude session UUID.
getSessionId(chatId)— returns existing or creates newresetSession(chatId)— generates fresh UUID- Persists to
data/sessions.json
Defines Jarvis's personality and behavior. Updated to reference native Task tool instead of MCP:
- Be concise for Telegram
- Delegate to sub-agents via Task tool
- Reference memory naturally
- Use update-memory skill for writes
Persistent facts injected via --append-system-prompt-file. Sections: User, Preferences, Active Context, People, Notes. Starts nearly empty, grows over time.
Native Claude Code agent definition:
---
name: ping-pong
description: "Test agent. Responds ping to pong and pong to ping."
tools: []
---
You are a simple test agent.
If the input contains "ping", respond with exactly "pong".
If the input contains "pong", respond with exactly "ping".
Nothing else.No tools, no session, no MCP. The main agent invokes it via the built-in Task tool automatically.
Skill that teaches the main agent how to write to memory:
---
name: update-memory
description: "Update long-term memory with new facts or preferences"
allowed-tools: Bash
---
To save something to memory, append to the memory file using:
echo "- <fact>" >> agents/main/memory.md
Insert under the appropriate section header (## User, ## Preferences, etc).Claude Code project-level settings:
- Allowed tools scoped to what Jarvis needs
- Model preference (if needed)
- Any permission grants for Bash/file operations
macOS LaunchAgent:
- Runs
node dist/index.js KeepAlive: true— auto-restart on crashRunAtLoad: true— starts on login- Working directory: project root
- Logs to
data/logs/
Install script copies plist to ~/Library/LaunchAgents/ and loads it.
User sends "ask the ping pong agent: ping"
→ Telegram API
→ Relay (auth check, get session ID)
→ claude -p --resume <session> \
--setting-sources project \
--system-prompt-file agents/main/system-prompt.md \
--append-system-prompt-file agents/main/memory.md
→ Main agent sees Task tool (built-in)
→ Main agent calls Task(agent_type="ping-pong", prompt="ping")
→ Claude Code spawns .claude/agents/ping-pong.md in isolated context
→ Returns "pong"
→ Main agent responds: "The ping pong agent says: pong"
→ CLI runner captures stdout
→ Relay chunks if >4096 chars
→ Relay sends to Telegram
User sees: "The ping pong agent says: pong"
Memory write flow:
User says "remember that I prefer dark mode"
→ Main agent recognizes this as a memory-worthy fact
→ Main agent uses Bash tool (allowed via skill):
echo "- Prefers dark mode" >> agents/main/memory.md
→ Responds: "Got it, I'll remember you prefer dark mode"
→ Next message: memory.md is re-injected via --append-system-prompt-file
jarvis/
├── .claude/
│ ├── settings.json # Project-level CC settings (permissions, model)
│ ├── agents/
│ │ └── ping-pong.md # Native sub-agent definition
│ └── skills/
│ └── update-memory/
│ └── SKILL.md # Memory write skill
├── src/
│ ├── index.ts # Telegram relay + entry point
│ ├── cli-runner.ts # claude -p wrapper (simplified)
│ └── session-store.ts # Chat ID → session ID mapping
├── agents/
│ └── main/
│ ├── system-prompt.md # Main agent personality + instructions
│ └── memory.md # Persistent memory (injected as context)
├── daemon/
│ ├── com.jarvis.plist # launchd config
│ └── install.sh # Install/uninstall script
├── data/
│ ├── logs/ # stdout/stderr from daemon
│ └── sessions.json # Session store
├── package.json # telegraf, dotenv
├── tsconfig.json
├── .env # TELEGRAM_BOT_TOKEN, TELEGRAM_ALLOWED_USERS
└── .env.example
— Native Task tool replaces custom spawn_agentsrc/mcp-server.ts— No MCP server to configureconfig/mcp.json— Replaced byconfig/agents.json.claude/agents/*.md— Replaced byagents/ping-pong/system-prompt.md.claude/agents/ping-pong.md— Removed from dependencies@modelcontextprotocol/sdk
- Clean up existing files (remove MCP artifacts)
- Create
.claude/directory structure - Create
.claude/settings.json - Update
package.json(remove MCP SDK dep) - Verify TypeScript compiles
- Create
.claude/agents/ping-pong.md - Test manually:
cd jarvis && claude -p "delegate to ping-pong: ping" - Verify Task tool delegation works
- Create
.claude/skills/update-memory/SKILL.md - Test manually:
claude -p "remember that I like TypeScript" - Verify
agents/main/memory.mdgets updated
- Rewrite
src/cli-runner.ts— singlerunMainAgent()function - Use
--setting-sources projectfor isolation - Use
--system-prompt-file+--append-system-prompt-file - Use
--output-format jsonfor session ID capture - Handle timeouts and errors
- Already done (
src/session-store.ts) - Minor tweaks if needed
- Complete
src/index.ts - Auth middleware (user ID allowlist)
- Message handler →
runMainAgent()→ reply /resetcommand- Typing indicator
- Response chunking (4096 char limit)
- Error handling
npm run build && node dist/index.js- Telegram → "ask ping pong: ping" → "pong"
- Telegram → "remember I like cats" → memory updated
- Telegram → "what do you know about me?" → references memory
/reset→ new session
daemon/com.jarvis.plistdaemon/install.sh(load/unload)- Verify auto-restart on crash
- Verify log rotation
- Error messages that make sense to the user
- Timeout handling (configurable, default 2min)
- Graceful shutdown (SIGTERM handling)
- Update system prompt for production use
{
"dependencies": {
"telegraf": "^4.16.0",
"dotenv": "^16.4.0"
},
"devDependencies": {
"typescript": "^5.4.0",
"@types/node": "^22.0.0"
}
}That's it. Two runtime deps. Claude Code CLI is the only external binary.
TELEGRAM_BOT_TOKEN= # From @BotFather
TELEGRAM_ALLOWED_USERS= # Comma-separated Telegram user IDs
JARVIS_HOME= # Project root (defaults to cwd)- ✅
--resume+-p+--append-system-prompt-file— All flags compose freely (confirmed in docs) - ✅
--tools ""— Empty string disables all built-in tools (confirmed in CLI reference) - ✅ Native subagents — Built-in Task tool +
.claude/agents/eliminates custom MCP server - ✅ Config isolation —
--setting-sources project+--system-prompt-filekeeps personal CC untouched - ✅ Telegram chunking — Split at 4096 chars on paragraph/line boundaries
- More sub-agents: web search, code review, calendar, etc.
- Cron-triggered agents (scheduled tasks via launchd)
- Richer Telegram UI: inline keyboards, callbacks
- MCP servers for external integrations (if needed later)
- Image/file handling via Telegram