Turn Claude Code into a persistent personal AI agent with continuous heartbeat, self-evolving memory, and bidirectional IM integration.
If you're reading this via an AI assistant (Claude Code, Cursor, etc), paste this to it:
Clone
https://github.com/phronesis-io/pascal-jarvisinto my working directory, cd into it, run./setup.sh, then walk me through the plugin setup and editingjarvis.yaml.
The setup.sh wizard is non-interactive, idempotent, and safe to re-run. It:
- Checks for
python3/jq/pip3(prints install commands if missing) - Installs
pyyaml(with--break-system-packagesfallback for modern macOS/Debian) - Makes shell scripts executable
- Copies
jarvis.example.yaml → jarvis.yamlif missing (never overwrites) - Seeds the memory directory with example templates
- Runs the test suite as a sanity check
- Prints clear "next steps" — including the plugin wizards below
Each plugin has its own interactive installer — both are optional, and headless mode (no plugins) works fine:
Lark (Feishu) — chat with your bot from your phone
npm install -g @larksuite/cli
npx skills add larksuite/cli -y -g
lark-cli config init --new # creates Lark app (browser auth)
lark-cli auth login --recommend # grants scopes (browser auth)
# then paste your open_id into jarvis.yamlFull walkthrough: plugins/lark/README.md
EigenFlux — broadcast network for AI agents
curl -fsSL https://www.eigenflux.ai/install.sh | sh # install CLI
eigenflux auth login --email you@example.com # email OTP flowAuth credentials stored in ~/.eigenflux/. Jarvis-specific settings in eigenflux/user_settings.json. Full walkthrough: plugins/eigenflux/README.md
After git pull, run the memory migration script if your memory is still in flat layout:
./scripts/migrate-memory.sh [YOUR_MEMORY_DIR]This restructures memory into hot/warm/timeline/system layers. Safe to run multiple times.
Also clone the EigenFlux skills repo if not already present:
git clone https://github.com/phronesis-io/openclaw-eigenflux ../openclaw-eigenfluxThen restart jarvis.
./bot.sh- With
lark.user_idset → Lark bot live - Without → heartbeat-only mode (memory consolidation + EigenFlux still run)
Pascal Jarvis wraps Claude Code with a full personal-agent runtime:
-
Heartbeat Loop + Guardian Daemon — A background scheduler runs tasks on configurable intervals (defined in
HEARTBEAT.md, executed via pre/post shell scripts + a batched Claude call). A guardian daemon (daemon.py) monitors the bot process, kills stuck Claude sessions, and auto-restarts on crash. -
Tiered Memory System — Five-layer memory that compresses over time (permanent → monthly → weekly → daily → hourly). Memory is injected into every Claude call, giving it persistent context across sessions.
-
Daily Rhythm & Calendar — A suite of time-aware tasks that structure the day:
- Daily plan — morning overview of calendar, priorities, and open threads (time-gated 8:00-9:30)
- Activity log — silent background tracker that logs what you're working on
- Daily reflect — evening review with wins, patterns, and tomorrow prep
- Free-time nudge — detects idle calendar blocks and sends casual suggestions
- Calendar read/write — 30-day rolling window (7 days detailed + 8-30 days compact), with create/update/delete write-back
- Task triage — philosophical task system (praxis/poiesis capture → commit → decay)
- Weekly review — end-of-week summary and planning
-
Built-in Plugins & Content Curation — Two first-class integrations plus content-aware features:
- Lark (Feishu) — bidirectional IM bridge so you can chat with your agent from your phone.
- EigenFlux — broadcast network with a two-stage pipeline: feed triage for quick scoring, plus deep research for high-value items.
- Content recommend and watchlater remind — curates and follows up on saved content.
Both plugins are optional — disable either by leaving its config section out of
jarvis.yaml. See the Plugins section below for usage. -
Self-Evolution — Engagement tracking analyzes which messages land and which don't, auto-tuning checkin frequency, content mix, and delivery windows. Cross-session sync imports context from parallel Claude Code projects.
-
Admin Console & Ops Tooling — Local web dashboard (
python3 admin.py) for browsing memory and session history. Background tasks handle repos sync, system self-diagnostics, and cross-session context bridging.
┌─────────────────────────────────────────────────────────────┐
│ daemon.py (guardian — monitors + restarts bot.sh) │
│ │
│ bot.sh (entry point) │
│ ├── Lark event listener (foreground) │
│ │ └── sources plugins/lark/client.sh → claude -p │
│ ├── Heartbeat loop (background) → core/heartbeat.py │
│ │ ├── Parse HEARTBEAT.md │
│ │ ├── Run pre-scripts (gather data) │
│ │ ├── Batch Claude call │
│ │ └── Run post-scripts (act on output) │
│ └── Admin console (background, optional) → admin.py │
│ │
│ core/ (system) │
│ ├── config.py — jarvis.yaml loader │
│ ├── heartbeat.py — task scheduler │
│ ├── memory.py — tiered memory loader │
│ ├── session.py — session rotation + fcntl lock │
│ ├── search.py — session history parser │
│ ├── safety.py — error-pattern filter │
│ ├── card.py — Lark card message builder │
│ ├── timeutil.py — timezone / time-range helpers │
│ └── jobs.py — job queue utilities │
│ │
│ handlers/ (message handlers) │
│ └── handle_image.sh — image message processing │
│ │
│ scripts/ (ops & dev tools) │
│ ├── backup_sessions.sh — daily session backup │
│ ├── memory-viewer.py — interactive memory browser │
│ ├── search_v2.py — enhanced transcript search │
│ ├── session_search.py — simple session search │
│ ├── tail_turns.py — tail recent conversation turns │
│ └── migrate-memory.sh — one-time memory migration │
│ │
│ plugins/ (built-in) │
│ ├── lark/ │
│ │ └── client.sh — shell helpers sourced by bot.sh │
│ └── eigenflux/ │
│ └── client.py — HTTP client + local persistence │
│ │
│ tasks/ (pre/post hooks) │
│ ├── Daily rhythm: │
│ │ daily_plan, activity_log, daily_reflect, free_time_nudge│
│ ├── Calendar & Tasks: │
│ │ calendar_sync, calendar_write, task_triage, │
│ │ weekly_review │
│ ├── Memory pipeline: │
│ │ memory_hourly → daily → weekly → monthly, │
│ │ memory_consolidate, memory_tidy │
│ ├── EigenFlux: │
│ │ feed, messages, publish, profile, research │
│ ├── Content: │
│ │ content_recommend, watchlater_remind │
│ └── Monitoring & ops: │
│ checkin, engagement_analyze, cross_session_sync, │
│ phronesis_monitor, repos_sync, self_diagnostic, │
│ personal_site │
└─────────────────────────────────────────────────────────────┘
-
Claude Code CLI — install and authenticate:
# macOS / Linux npm install -g @anthropic-ai/claude-code claude # follow the auth flow on first run
-
Python 3.10+ with PyYAML:
pip install pyyaml
-
jq (for Lark message parsing):
# macOS brew install jq # Ubuntu/Debian sudo apt install jq
-
(Optional) Plugins — both are built-in but opt-in:
- Lark — see plugins/lark/README.md for app creation +
lark-clisetup - EigenFlux — see plugins/eigenflux/README.md for account registration
You can run fully headless (no plugins) — memory consolidation still works.
- Lark — see plugins/lark/README.md for app creation +
# Clone the repo
git clone https://github.com/phronesis-io/pascal-jarvis.git
cd pascal-jarvis
# Create your config
cp jarvis.example.yaml jarvis.yaml
# Edit jarvis.yaml:
# - Set data_dir (where sessions/memory are stored)
# - Set work_dir (directory Claude can access — your project root)
# - (Optional) Set lark.user_id to your Lark open_id
# Set up initial memory (tiered structure)
mkdir -p ~/.jarvis/memory
cp -R examples/memory/* ~/.jarvis/memory/
# Edit the memory files to describe yourself
# (Optional) Set up built-in plugins — see their dedicated READMEs for full setup
# Lark: plugins/lark/README.md
# EigenFlux: plugins/eigenflux/README.md
# Make scripts executable
chmod +x bot.sh tasks/*.sh
# Run
./bot.shIf you don't set lark.user_id in jarvis.yaml, the bot runs in heartbeat-only mode — it still does memory consolidation, EigenFlux feed triage, and everything else, but without IM.
Create ~/Library/LaunchAgents/com.jarvis.daemon.plist — point at daemon.py (not bot.sh directly), so the guardian can monitor and auto-restart the bot:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.jarvis.daemon</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/path/to/pascal-jarvis/daemon.py</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/tmp/jarvis-daemon-stdout.log</string>
<key>StandardErrorPath</key><string>/tmp/jarvis-daemon-stderr.log</string>
</dict>
</plist>launchctl load ~/Library/LaunchAgents/com.jarvis.daemon.plistThis auto-starts on login. The daemon manages bot.sh lifecycle — you can also run ./bot.sh directly for development.
All config lives in jarvis.yaml. See jarvis.example.yaml for the full schema.
Key settings:
data_dir— where sessions and memory are storedlark.user_id— your Lark/Feishu open_id (skip for headless)claude.heartbeat_model— model for background tasks (default:sonnet, cheaper)heartbeat.check_interval— how often to check for due tasks (default: 10s)
Tasks are the building blocks of the heartbeat loop. Each task is defined in HEARTBEAT.md and implemented as a pair of scripts.
Add a block to HEARTBEAT.md:
### task-name
- interval: 10m # how often to run
- pre: tasks/task_name_pre.sh # data gathering script
- post: tasks/task_name_post.py # response handler
- prompt: |
Your prompt here.Tasks follow a strict naming pattern: tasks/<name>_pre.sh + tasks/<name>_post.py. The pre-script gathers data; the post-script acts on Claude's response.
The pre-script's stdout becomes Claude's input data for that task. Key rules:
- Empty stdout = skip task. If the pre-script prints nothing, the task is silently skipped and retried at the next interval. This is the primary gating mechanism.
- Time-gated tasks: Check the current hour and exit early. For example,
daily_plan_pre.shonly runs between 8:00-9:30; outside that window it exits with no output. - Pre-scripts typically call APIs, read files, or check system state to assemble context for Claude.
The post-script receives Claude's response on stdin and can act on it:
- stdout becomes the Lark message. If the post-script prints something, it gets sent to the user via Lark.
- Silent tasks: Post-scripts that write nothing to stdout (e.g.,
activity_log_post.py) perform their work silently — writing to files, updating state — without notifying the user. - Post-scripts can import from
core/andplugins/to call APIs, update memory, etc.
HEARTBEAT_OK is the universal "nothing to do" response. When Claude determines there's no actionable output for a task, it returns this string. Post-scripts should check for it and exit cleanly:
response = sys.stdin.read().strip()
if response == "HEARTBEAT_OK":
sys.exit(0)- Notify user: Pre-script gathers data → Claude analyzes → post-script prints a message → sent to Lark
- Silent tracking: Pre-script gathers data → Claude processes → post-script writes to a file, prints nothing
- Time-gated: Pre-script checks
date +%H, exits if outside window → task skipped entirely - API-gated: Pre-script calls an API, exits if no new data → task skipped until data appears
Jarvis ships with two built-in plugins that are integrated at the system level. Each has a dedicated README with full setup, API, and troubleshooting.
📖 Full docs: plugins/lark/README.md
Chat with your agent from Lark/Feishu on any device. The plugin:
- Subscribes to incoming messages (
im.message.receive_v1) - Maps each conversation (
conv_key) to a stable Claude Code session - Auto-rotates sessions when they cross
claude.max_session_size - Shows transient
Thinking...indicators during Claude calls - Recognizes shortcut commands (
loop/heartbeatto force-trigger a heartbeat cycle)
Enable — add to jarvis.yaml:
lark:
user_id: "ou_your_open_id"
app_id: "cli_your_app_id"Run — the bot picks it up automatically. Leave lark: out to run headless.
Shell API (sourced by bot.sh from plugins/lark/client.sh):
lark_send · lark_reply · lark_reply_text · lark_delete_message · lark_subscribe_messages · lark_freebusy
📖 Full docs: plugins/eigenflux/README.md
EigenFlux is a broadcast network where AI agents share and receive real-time signals. Five heartbeat tasks plus a real-time stream integrate it:
| Task | Interval | What it does |
|---|---|---|
eigenflux-feed-triage |
10m | Pull feed, score items, push actionable ones to you |
eigenflux-research |
30m | Deep analysis of items flagged as "needs research" |
eigenflux-messages |
10m | Fetch unread DMs, suggest responses |
eigenflux-publish |
1h | Auto-broadcast useful signals from your conversations |
eigenflux-profile |
24h | Sync your EigenFlux bio with memory changes |
Additionally, bot.sh runs a continuous EigenFlux stream (WebSocket) that delivers messages in real-time with background Claude analysis.
Enable — add to jarvis.yaml:
plugins:
eigenflux:
enabled: true
persist_feed: true
feed_db: eigenflux/feed_store.jsonlSetup — install the official eigenflux CLI and authenticate
(see the plugin README).
Programmatic access — call the CLI directly via plugins/eigenflux/client.sh
(bash) or python3 -m plugins.eigenflux.feed_search (Python). The plugin
intentionally has no standalone Python SDK — the CLI is the only API surface.
A plugin is just a directory under plugins/ that provides one or both of:
- A client wrapper (shell helpers around a CLI, or Python helpers) — the shared code task scripts import.
- Heartbeat tasks in
HEARTBEAT.md+ matchingtasks/<plugin>_*_pre.sh/_post.pyscripts.
Pre-scripts write to stdout (becomes Claude's input data); post-scripts read stdin (Claude's response) and can call the plugin's client library to act on it. If a post-script writes to stdout, that becomes the message sent to Lark. Follow the EigenFlux plugin structure as a template.
Utility scripts in scripts/ for operations and debugging:
backup_sessions.sh— Daily session file backup with read-only protection. Copies session transcripts to an archive directory so they survive rotation.memory-viewer.py— Interactive TUI for browsing the tiered memory tree. Useful for inspecting what the agent "knows" without digging through files.search_v2.py— Enhanced session transcript search with relevance scoring and context display.session_search.py— Simple session search tool for quick keyword lookups.tail_turns.py— Tail recent conversation turns, liketail -ffor live conversations. Helpful for monitoring what the bot is doing.migrate-memory.sh— One-time migration from flat memory layout to the tieredhot/warm/timeline/systemstructure. Safe to run multiple times (idempotent).
daemon.py is a lightweight supervisor process that keeps the bot alive:
- Monitors
bot.shhealth every 2 minutes — checks PID, heartbeat freshness, and session locks - Kills stuck Claude processes by detecting stale session lock files
- Auto-restarts
bot.shon crash (up to 3 attempts with 5-minute cooldown) - Logs to
daemon.logwith automatic log rotation
Run with:
python3 daemon.pyThe daemon manages bot.sh directly — you don't need to run bot.sh separately. For production use, point your launchd plist at daemon.py instead of bot.sh.
Memory files live in ~/.jarvis/memory/ (or your configured data_dir/memory/).
- Hourly: Indexes the last hour's conversation into 1-3 lookup lines
- Daily: Compresses hourly entries into 3-6 bullet points
- Weekly: Merges daily entries into a 5-10 point digest
- Monthly: Compresses weekly digest into a long-term archive
- Consolidation: Nightly review that proposes updates to permanent memory files
Each layer archives before clearing, so nothing is ever lost.
Create a .md file in the memory directory with frontmatter:
---
name: My Project
description: One-line description used for relevance matching
type: project
---
Your content here.Types: user, feedback, project, reference
A local web dashboard for browsing memories, searching session history, and viewing skills/settings.
python3 admin.py
# open http://localhost:3456Configure host/port in jarvis.yaml under the admin: section. Config-driven: it reads memory_dir and derives the sessions path from work_dir, so it always matches your bot's view.
Bot stuck on "Thinking..." forever
- Check
jarvis.logfor errors - Verify
work_dirinjarvis.yamlmatches where your Claude Code sessions live (~/.claude/projects/<hash>/) - Delete
active_sessions.jsonto start fresh sessions in the correct project dir
[SDK Error] handle message failed in logs
- Benign — lark-cli receives event types (like
message_read_v1) it doesn't have a handler for. The bot ignores these.
Heartbeat not running tasks
- Check
heartbeat_state.jsonfor last-run timestamps - Delete it to force all tasks to run on next cycle
- Tasks also skip if their pre-script exits with empty output (see
tasks/*.sh)
Tests
python3 -m pytest tests/MIT