Skip to content

Latest commit

 

History

History
200 lines (157 loc) · 10.5 KB

File metadata and controls

200 lines (157 loc) · 10.5 KB

etwin-bot

A single-user, proactive Telegram companion with model-led timing.

English | 中文

etwin-bot wakes on a configurable timer, gives recent conversation and interaction context to a Claude, Codex, or AGY backend, and lets the model decide whether to reach out or stay quiet. Cadence is mostly model-led, but not absolute: an explicit /quiet command is enforced in code for 24 hours before any LLM call.

Design Principles

  • Model-led cadence. The bot wakes periodically; the model reads current context, prompt policy, and interaction history before choosing ping or silent.
  • Deterministic owner override. /quiet blocks proactive ticks for 24 hours without spending a provider call. Other cadence guidance stays in the selected prompt/persona.
  • Personal persona. The public repo ships neutral templates; private profile details can live in gitignored .local.md overrides.
  • Swappable backend. Claude Agent SDK, codex exec, and the AGY CLI are supported. Each uses its existing host login instead of a project API key.
  • Single-user Telegram boundary. Non-dry-run startup requires an exact ALICE_CHAT_ID, and every tool-bearing text/media handler checks it. Use a dedicated bot token and chat.

Architecture

                  ┌──────────────┐
   You on TG ────►│  bot.js      │ grammy reactive conversation
                  │  (grammy)    │
                  └──────┬───────┘
                         │
                         ▼
                  ┌──────────────┐
                  │ self-loop.js │ wakes on a configured interval
                  │  (interval)  │ → LLM decides ping / silent
                  └──────┬───────┘
                         │
                         ▼
                  ┌──────────────┐
                  │ context.js   │ gathers RecallNest checkpoints / CC activity / git activity / action log
                  └──────┬───────┘
                         │
                         ▼
                  ┌──────────────┐
                  │   llm.js     │ Claude SDK / codex exec / AGY CLI
                  └──────────────┘

The tracked env examples use a four-hour interval (14400000 ms). If the variable is omitted, the code fallback is 30 minutes.

File Structure

etwin-bot/
├── bot.js                    main entry: grammy + self-loop
├── paths.js                  per-instance runtime paths (data / files separation)
├── runtime-files.js          contained, collision-resistant inbound/output paths
├── self-loop.js              proactive driver: wake → LLM decides → act
├── context.js                collects state to feed the LLM
├── llm.js                    Claude SDK / Codex / AGY backend routing
├── agy-backend.js            AGY CLI invocation, timeout, and JSON parsing
├── image-generation.js       explicit image request routing
├── interaction.js            quiet-window and reaction statistics
├── message-split.js          Telegram-safe response segmentation
├── deploy/                   path-free launchd plist templates
├── install-launchd.sh        renders local LaunchAgent files
├── persona/
│   ├── digital-clone-base.md     public neutral persona base
│   ├── digital-clone-profile.md  public neutral operator profile
│   └── e-tuning.md               extraversion tuning layer (unique to this bot)
├── prompts/
│   ├── self-decision.md      self-loop decision prompt (emits JSON)
│   └── reply.md              conversation prompt for direct messages (emits natural language)
├── data/
│   ├── action-log.json       the bot's own send history + your reactions, fed back into the next decision
│   └── conversation-history.json  recent conversation (last ~30 turns)
├── package.json
├── start.sh                  dev launch script
├── .env.example
├── .env.codex.example        Codex-backend instance template
├── .env.agy.example          AGY-backend instance template
└── README.md

Persona files. The two tracked digital-clone files are regular, neutral templates, so the default mode works in a clean clone. A richer corpus can be maintained separately in digital-clone-skill. Next to any foo.md, you can add a gitignored foo.local.md; the runtime loads it first, keeping private tuning out of the public repo.

Quick Start

# 1. Copy the env template and fill in real values
cp .env.example .env
# edit .env, set TG_BOT_TOKEN (from @BotFather)

# 2. Install dependencies
bun install

# 3. Dry-run once (no provider call or proactive send)
ETWIN_DRY_RUN=true bun run bot.js
# dry-run still connects to Telegram, replies to /start/text, and can update local action state
# send the bot /start and read its reply to get your chat ID
# put it in .env as ALICE_CHAT_ID

# 4. Fire one provider-backed self-tick to test the configured LLM path
bun run tick
# this command makes a real backend call unless ETWIN_DRY_RUN=true

# 5. Go live
# edit .env, set ETWIN_DRY_RUN=false
bash start.sh

Security Boundary

This project intentionally gives a personal bot meaningful host access, so the trust boundary matters more than the bot UI:

  • ALICE_CHAT_ID is mandatory outside dry-run, and media/text handlers repeat the same exact-chat check.
  • Claude full mode uses bypassPermissions; Codex full mode can be configured as danger-full-access; the initial AGY path keeps the CLI's default permissions and does not pass a permission-bypass flag. The bot can therefore act with the permissions available to the host account and selected backend.
  • Telegram files are downloaded locally, then relevant prompts/files are sent to the selected Claude, Codex, or AGY path. This is not an offline-only data flow.
  • Inbound names are sanitized and resolved under ETWIN_FILE_DIR; generated image outputs use contained, collision-resistant names.
  • Keep the bot token, chat ID, runtime data, and private .local.md files out of the repository. Do not reuse this configuration as a public or multi-user bot.

Cadence Policy

The selected self-decision prompt and persona carry the ordinary timing policy. The current E-tuning, Codex, and AGY guidance treats 02:00–06:00 as a sleep window, backs off after recent conversation or repeated unread pings, and rejects empty check-ins. These are model instructions and can vary by prompt; only an active /quiet request is a deterministic code gate.

When the Bot Drifts

  1. Read data/action-log.json — every decision carries its reasoning, so you can see how it was thinking.
  2. Edit persona/e-tuning.md to adjust the tendencies, then restart.
  3. Send /quiet to pause proactive messages — the runtime records a 24-hour quiet window and skips the LLM while it is active.
  4. If you just don't like it, set ETWIN_PROACTIVE=false for pure reactive mode: the bot only replies, never initiates.

Multiple Instances

Three backends are supported, with a separate launchd service identity for each configuration:

  • com.etwin-bot — the original Claude-backed version, uses .env, keeps proactive mode.
  • com.etwin-codex-bot — the Codex version, loading .env.codex.
  • com.etwin-agy-bot — the AGY version named etwin-agy, loading .env.agy and using separate logs and data directories.

The Codex version stores incoming Telegram images in files-codex/ and passes them to the backend via codex exec --image <path>; an image with no caption is held until the next text message.

Key env for the Codex version:

ETWIN_INSTANCE=codex
ETWIN_PERSONA=codex
ETWIN_LLM_BACKEND=codex
ETWIN_REPLY_PROMPT=prompts/reply-codex.md
ETWIN_DATA_DIR=data-codex
ETWIN_FILE_DIR=files-codex
ETWIN_PROACTIVE=true
ETWIN_RUN_ON_START=false
ETWIN_SELF_PROMPT=prompts/self-decision-codex.md

Launch a dev instance:

ETWIN_ENV_FILE=.env.codex bash start.sh

The launchd template is at deploy/com.etwin-codex-bot.plist.template.

Key AGY env (see .env.agy.example for the complete template):

ETWIN_INSTANCE=agy
ETWIN_DISPLAY_NAME="etwin-agy"
ETWIN_PERSONA=agy
ETWIN_LLM_BACKEND=agy
ETWIN_REPLY_PROMPT=prompts/reply-agy.md
ETWIN_SELF_PROMPT=prompts/self-decision-agy.md
ETWIN_AGY_TIMEOUT_MS=600000

AGY starts a fresh conversation for every call. E-Twin already places recent Telegram history in the prompt, so it does not pass --conversation and duplicate that context. If ETWIN_AGY_MODEL and ETWIN_AGY_EFFORT are unset, the host AGY defaults are used.

Launch a development AGY instance with ETWIN_ENV_FILE=.env.agy bash start.sh. Its launchd template is deploy/com.etwin-agy-bot.plist.template.

Deployment

Runs on a Mac mini under launchd. Claude, Codex, and AGY have separate templates, while only the desired Telegram identities need to be loaded. The tracked templates contain placeholders rather than a username or checkout path. Render the LaunchAgents for the current machine, then load the desired services:

./install-launchd.sh
launchctl bootout "gui/$(id -u)" "$HOME/Library/LaunchAgents/com.etwin-bot.plist" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/com.etwin-bot.plist"
launchctl bootout "gui/$(id -u)" "$HOME/Library/LaunchAgents/com.etwin-agy-bot.plist" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/com.etwin-agy-bot.plist"

Claude SDK calls are aborted after ETWIN_CLAUDE_TIMEOUT_MS. ETWIN_AGY_TIMEOUT_MS is the AGY CLI's soft timeout (ten minutes by default); the Node wrapper allows one additional minute before terminating a stuck process. Timer ticks are non-overlapping: a still-running proactive call makes the next scheduled tick skip instead of starting a second model call.

Non-Goals

  • Doesn't touch telegram-ai-bridge — a separate bot, kept isolated.
  • Doesn't touch wechat-ai-bridge — the iLink protocol disallows proactive messages, so this line isn't wired there.
  • No chat-log decryption. A privacy boundary, and unnecessary: RecallNest plus Claude Code session logs are enough to sense state.
  • No screenshots, camera, or video-call surveillance.
  • No multi-user or hosted-service mode. Explicit work requests may enter the local full-tool path; that path is intentionally personal and high privilege.