personal experiment with a claw
README.md

disclaw #

My very own claw. Super experimental and only really meant for me for now.

A personal Claude-style agent that lives on my Mac mini, talks back through Discord, and remembers things between conversations. Built on the pi agent harness.

Architecture #

Mac mini
└── docker compose
    └── disclaw (one Node process)
        ├── Pi SDK ........... agent runtime + tools (bash, edit, read, write)
        ├── Discord client ... DM / mention triggered
        ├── Lane manager ..... one session per channel; persisted in .pi/; steers mid-turn
        └── Scheduler ........ croner, fires fresh sessions on cron
        + volumes:
          memory/  persistent markdown notes + scheduled jobs
          .pi/     persistent Pi session files + lane index
          skills/  read-only mount of skill instructions

Prerequisites #

  • Node 20+ (for local dev) — Docker handles this in deployment
  • Docker + Docker Compose (for deployment)
  • An Anthropic API key (or another Pi-supported provider)
  • A Discord application + bot token

Quick start — local CLI #

For testing memory and schedules before setting up Discord.

cp .env.example .env
# fill in ANTHROPIC_API_KEY
npm install
npm run dev:cli

You should see skills: ..., memory, schedule in the boot output. Try:

  • remember that I prefer Go over Python — writes memory/<topic>.md, updates INDEX.md
  • what do you remember about me? — re-reads INDEX.md and any relevant memory files
  • list_schedules — exercises the schedule tool

/exit to quit.

Discord bot setup #

  1. https://discord.com/developers/applications → New Application.
  2. Bot tab → Reset Token → copy. Paste into DISCORD_BOT_TOKEN in .env.
  3. Bot tab → Privileged Gateway Intents → enable MESSAGE CONTENT INTENT.
  4. OAuth2 → URL Generator → scope bot, permissions View Channel + Send Messages + Read Message History + Add Reactions. Open the generated URL and add the bot to a server you control. The bot must share a server with you for DMs to work — there is no DM-only path.
  5. In Discord: User Settings → Advanced → enable Developer Mode. Right-click your own name → Copy User ID. Paste into DISCORD_ALLOWED_USER_IDS in .env (comma-separated for multiple).

Run it:

npm run dev

The bot responds to DMs from allowed users and to @-mentions in any server channel it's been added to. All other messages are silently ignored. Each Discord channel/thread keeps its own Pi session history, and that history survives bot restarts via .pi/sessions/.

Mac mini deployment #

  1. Stop the mini from sleeping (cron won't fire if it does):
    sudo pmset -a sleep 0
    sudo pmset -a displaysleep 10
    
  2. Set TZ in .env so cron expressions match wall clock:
    TZ=America/Los_Angeles
    
  3. Build and start:
    docker compose up --build -d
    docker compose logs -f
    
  4. Confirm disclaw bot ready as ... and scheduler: N job(s) registered in the logs.

Update after pulling new code:

docker compose up --build -d

Stop:

docker compose down

Memory #

The agent reads/writes plain markdown files in memory/. Format is documented in skills/memory/SKILL.md. Short version:

  • memory/INDEX.md is always loaded; one-line pointers to every memory file.
  • Each topic is its own memory/<topic>.md with name / description / type frontmatter.
  • Types: user, feedback, project, reference.

Edit memory files directly in your editor whenever you want — the agent picks up changes the next time it reads them.

Schedules #

Tell the agent things like "every Tuesday at 9am, summarize my Twitter feed" — it writes memory/schedules/<name>.md with a cron expression and prompt. The scheduler watches that directory and fires each job in a fresh agent session at its cron time, posting the result back to the Discord channel where you scheduled it.

Edit a schedule file directly to tweak the prompt or cron — the scheduler reloads on filesystem change.

Cron is standard 5-field (min hour dom mon dow):

Expression Means
0 9 * * 2 Tuesday 9am
0 9 * * * Daily 9am
0 9 * * 1-5 Weekdays 9am
*/30 * * * * Every 30 minutes

Customization #

  • Model: set DISCLAW_MODEL (and optionally DISCLAW_PROVIDER) in .env. Default is anthropic/claude-sonnet-4-6.
  • New skill: drop a directory under skills/<name>/SKILL.md with frontmatter (name, description). Bind-mount means no rebuild — restart the container to pick up.
  • New tool: add a defineTool in src/tools/, register it in src/agent.ts → createSession.

File layout #

src/
├── agent.ts          shared deps + session factory
├── bot.ts            Discord client + allowlist + dispatch
├── cli.ts            terminal mode for local testing
├── lanes.ts          per-channel session map; steers if streaming
├── scheduler.ts      croner-based scheduler over memory/schedules/
├── tools/schedule.ts schedule_task / list_schedules / cancel_schedule
└── util.ts           Discord chunking + schedule file IO

skills/
├── memory/SKILL.md
└── schedule/SKILL.md

memory/                persistent (Docker bind-mount)
├── INDEX.md
└── schedules/         one .md per cron job

.pi/                   persistent (Docker bind-mount)
├── lane-sessions.json channel/thread/cli lane -> Pi session file
└── sessions/          Pi JSONL session files for each lane

Troubleshooting #

  • Bot doesn't respond. Check DISCORD_ALLOWED_USER_IDS matches your user ID exactly. Confirm MESSAGE CONTENT INTENT is enabled in the dev portal. In a server, the bot only responds to @-mentions, not all messages.
  • Model not found. DISCLAW_MODEL isn't in Pi's known list. Try the default claude-sonnet-4-6, or check node_modules/@earendil-works/pi-ai/dist/models.generated.js.
  • Scheduler not firing. Verify docker compose logs disclaw shows N job(s) registered. Confirm TZ is correct and sudo pmset -g | grep sleep shows sleep 0.
  • Schedule fires but posts nothing visible. The destination channel ID is probably wrong or the bot lost access to the channel. Open memory/schedules/<name>.md and fix the destination field.
  • Two messages collide on one channel. That's the lane-steering path: the second message is injected into the running turn. The bot reacts ⏳ on the original message to acknowledge.