diff --git a/docs/guides/claude-mem-setup.md b/docs/guides/claude-mem-setup.md new file mode 100644 index 000000000..320192505 --- /dev/null +++ b/docs/guides/claude-mem-setup.md @@ -0,0 +1,390 @@ +# Clawdbot + Claude-Mem Setup Guide + +Complete setup guide for integrating Clawdbot with Claude-Mem persistent memory. + +--- + +## Overview + +This guide walks you through: +1. Installing Clawdbot (AI messaging gateway) +2. Installing Claude-Mem (persistent memory system) +3. Connecting them via the `memory-claudemem` plugin + +**What you get:** +- Clawdbot routes messages between you and AI across multiple channels (Telegram, Discord, WhatsApp, etc.) +- Claude-Mem captures observations from every tool call and injects relevant context into future sessions +- Progressive disclosure memory tools (`memory_search`, `memory_observations`) + +--- + +## Prerequisites + +- **Node.js 22+** (required for both Clawdbot and Claude-Mem) +- **macOS, Linux, or Windows (WSL2)** +- **Git** (for installing from source) + +Check your Node version: +```bash +node -v +# Should show v22.x.x or higher +``` + +--- + +## Part 1: Install Clawdbot + +### Option A: Quick Install (Recommended) + +```bash +curl -fsSL https://clawd.bot/install.sh | bash +``` + +Windows (PowerShell): +```powershell +iwr -useb https://clawd.bot/install.ps1 | iex +``` + +This: +- Installs Node 22+ if needed +- Installs `clawdbot` globally via npm +- Runs the onboarding wizard + +### Option B: Manual Install + +If you already have Node 22+: + +```bash +npm install -g clawdbot@latest +``` + +Then run onboarding: + +```bash +clawdbot onboard --install-daemon +``` + +### Verify Installation + +```bash +clawdbot --version +clawdbot doctor +``` + +### Common Issue: "clawdbot not found" + +If your shell can't find `clawdbot`, add npm's global bin to your PATH: + +```bash +# Add to ~/.zshrc or ~/.bashrc +export PATH="$(npm prefix -g)/bin:$PATH" +``` + +Then restart your terminal or run `source ~/.zshrc`. + +--- + +## Part 2: Install Claude-Mem + +Claude-Mem is a separate system that provides persistent memory. You need to install it independently. + +### Option A: Via Claude Code Plugin (if using Claude Code) + +If you use Claude Code (Anthropic's CLI), install claude-mem as a plugin: + +```bash +# In a Claude Code session: +/plugin marketplace add thedotmack/claude-mem +/plugin install claude-mem +``` + +Restart Claude Code. The worker starts automatically. + +### Option B: Manual Installation (Standalone) + +Clone the repository: + +```bash +cd ~/Scripts # or wherever you keep projects +git clone https://github.com/thedotmack/claude-mem.git +cd claude-mem +``` + +Install dependencies and build: + +```bash +npm install +npm run build +``` + +Start the worker: + +```bash +npm run worker +# Or run in background: +nohup npm run worker > /tmp/claude-mem-worker.log 2>&1 & +``` + +### Verify Claude-Mem is Running + +The worker runs on port 37777 by default. + +```bash +curl http://localhost:37777/api/health +# Should return: OK or {"status":"ok"} +``` + +Open the web viewer: +```bash +open http://localhost:37777 +``` + +--- + +## Part 3: Enable the Memory-Claudemem Plugin + +The `memory-claudemem` plugin connects Clawdbot to the Claude-Mem worker. + +### Step 1: Disable Built-in Memory (Optional but Recommended) + +Clawdbot has its own memory system. To avoid conflicts, disable it: + +```bash +clawdbot config set agents.defaults.memorySearch.enabled false +``` + +Verify: +```bash +clawdbot config get agents.defaults.memorySearch.enabled +# Should return: false +``` + +### Step 2: Enable the Claude-Mem Plugin + +The plugin is bundled in the `extensions/memory-claudemem` directory. Enable it: + +```bash +clawdbot config set plugins.memory-claudemem.workerUrl "http://localhost:37777" +``` + +Optional: Adjust timeout (default 10 seconds): +```bash +clawdbot config set plugins.memory-claudemem.workerTimeout 15000 +``` + +### Step 3: Verify Plugin Status + +Check the worker connection: + +```bash +clawdbot claude-mem status +``` + +Expected output: +``` +✓ Worker running at http://localhost:37777 +``` + +If you see `✗ Worker not responding`, make sure Claude-Mem is running (see Part 2). + +--- + +## Part 4: Test the Integration + +### Test 1: Search Memories + +```bash +clawdbot claude-mem search "authentication" +``` + +This queries Claude-Mem for observations matching "authentication". + +### Test 2: Run an Agent with Memory + +Send a test message through Clawdbot: + +```bash +clawdbot agent --message "List files in the current directory" +``` + +This should: +1. Execute the task +2. Record the observation to Claude-Mem (fire-and-forget) +3. Future queries will have this context injected + +### Test 3: Verify Observation Recording + +Open the Claude-Mem web viewer: +```bash +open http://localhost:37777 +``` + +You should see new observations for tool calls made by Clawdbot. + +--- + +## Configuration Reference + +### Full Config Example + +Edit `~/.clawdbot/config.yml`: + +```yaml +# Disable built-in memory (use claude-mem instead) +agents: + defaults: + memorySearch: + enabled: false + +# Enable claude-mem plugin +plugins: + memory-claudemem: + workerUrl: http://localhost:37777 + workerTimeout: 10000 +``` + +### Rollback to Built-in Memory + +If you want to switch back to Clawdbot's built-in memory: + +```yaml +agents: + defaults: + memorySearch: + enabled: true + +plugins: + memory-claudemem: + enabled: false +``` + +--- + +## Available Tools + +When the plugin is active, these tools are available to the AI agent: + +| Tool | Description | +|------|-------------| +| `memory_search` | Search past observations. Returns compact results with IDs. | +| `memory_observations` | Get full details for specific observation IDs. | + +### 3-Layer Workflow + +1. **Search** (`memory_search`): Get index with IDs (~50-100 tokens/result) +2. **Filter**: Review results, identify relevant IDs +3. **Fetch** (`memory_observations`): Get full details (~500-1000 tokens/result) + +This pattern saves ~10x tokens compared to fetching everything. + +--- + +## CLI Commands + +| Command | Description | +|---------|-------------| +| `clawdbot claude-mem status` | Check if the worker is responding | +| `clawdbot claude-mem search ` | Search memories (JSON output) | + +Options for search: +```bash +clawdbot claude-mem search "authentication" --limit 20 +``` + +--- + +## Troubleshooting + +### Worker Not Responding + +1. Check if Claude-Mem is running: + ```bash + curl http://localhost:37777/api/health + ``` + +2. If not running, start it: + ```bash + cd ~/Scripts/claude-mem + npm run worker + ``` + +3. Check logs: + ```bash + tail -f /tmp/claude-mem-worker.log + ``` + +### Plugin Not Loading + +1. Check plugin list: + ```bash + clawdbot plugins list + ``` + +2. Look for `memory-claudemem` in the output. + +3. If missing, check the config: + ```bash + clawdbot config get plugins.memory-claudemem + ``` + +### No Observations Being Recorded + +1. Enable debug logging: + ```bash + clawdbot config set logging.level debug + ``` + +2. Run a command and check logs: + ```bash + clawdbot agent --message "test" + tail -f ~/.clawdbot/logs/gateway.log | grep claude-mem + ``` + +### Context Not Being Injected + +1. Check that there are observations in Claude-Mem (via web UI at http://localhost:37777) +2. Ensure the prompt is longer than 5 characters (short prompts skip injection) +3. Check logs for "injecting X memories into context" + +--- + +## Architecture + +``` +┌──────────────────┐ ┌──────────────────┐ +│ Clawdbot │ │ Claude-Mem │ +│ Gateway │ │ Worker │ +├──────────────────┤ ├──────────────────┤ +│ │ │ │ +│ before_agent │────▶│ context inject │ +│ after_tool_call │────▶│ POST /observe │ +│ │ │ │ +│ memory_search │────▶│ GET /search │ +│ memory_observations──▶│ POST /batch │ +│ │ │ │ +└──────────────────┘ └──────────────────┘ + │ │ + └────────────────────────┘ + http://localhost:37777 +``` + +**Hooks:** +- `before_agent_start`: Injects relevant memories as context +- `after_tool_call`: Records observations (fire-and-forget) + +**Tools:** +- `memory_search`: Queries Claude-Mem's search API +- `memory_observations`: Fetches full observation details + +--- + +## Next Steps + +- **Set up messaging channels**: `clawdbot channels add telegram` (or discord, slack, etc.) +- **Configure the AI model**: `clawdbot config set agents.defaults.model claude-sonnet-4-20250514` +- **Start the gateway**: `clawdbot gateway run` +- **Open the dashboard**: `clawdbot dashboard` + +For more information: +- Clawdbot docs: https://docs.clawd.bot +- Claude-Mem docs: https://docs.claude-mem.ai diff --git a/docs/plans/claude-mem-integration.md b/docs/plans/claude-mem-integration.md new file mode 100644 index 000000000..55de76ae7 --- /dev/null +++ b/docs/plans/claude-mem-integration.md @@ -0,0 +1,619 @@ +# Claude-Mem Integration Plan + +Integrate `thedotmack/claude-mem` as the primary observation agent and memory layer for Clawdbot. + +--- + +## Phase 0: Documentation Discovery + +### Allowed APIs (Clawdbot Plugin System) + +**Source:** `/home/user/clawdbot/src/plugins/types.ts:235-274` + +| API | Signature | Use Case | +|-----|-----------|----------| +| `api.registerTool` | `(tool: AnyAgentTool \| ClawdbotPluginToolFactory, opts?: ClawdbotPluginToolOptions) => void` | Register memory tools | +| `api.on` | `(hookName: K, handler: PluginHookHandlerMap[K]) => void` | Register lifecycle hooks | +| `api.registerCli` | `(registrar: ClawdbotPluginCliRegistrar, opts?: { commands?: string[] }) => void` | Add CLI commands | +| `api.registerService` | `(service: ClawdbotPluginService) => void` | Health check service | +| `api.logger` | `PluginLogger` | Logging (`info`, `warn`, `debug`) | +| `api.pluginConfig` | `Record` | Plugin configuration | + +### Available Hooks + +**Source:** `/home/user/clawdbot/src/plugins/types.ts:289-303` + +| Hook | Execution | Clawdbot → Claude-Mem | +|------|-----------|----------------------| +| `session_start` | Parallel (fire-and-forget) | → `SessionStart` | +| `before_agent_start` | Sequential (can modify) | → `UserPromptSubmit` + context injection | +| `after_tool_call` | Parallel (fire-and-forget) | → `PostToolUse` | +| `agent_end` | Parallel (fire-and-forget) | → `Stop` | +| `session_end` | Parallel (fire-and-forget) | → `SessionEnd` | + +### Allowed APIs (Claude-Mem Worker) + +**Source:** GitHub Issue #348, Platform Integration Guide + +| Endpoint | Method | Request | Response | +|----------|--------|---------|----------| +| `/api/health` | GET | - | 200 OK | +| `/api/sessions/observations` | POST | `{ claudeSessionId, tool_name, tool_input, tool_response, cwd? }` | `{ id }` | +| `/api/search` | GET | `?query=&type=observations&format=index&limit=` | `SearchResult[]` | +| `/api/observations/batch` | POST | `{ ids: number[], orderBy?, limit? }` | `Observation[]` | + +**Note:** `created_at_epoch` is in MILLISECONDS (not seconds). + +### Memory Disable Mechanism + +**Source:** `/home/user/clawdbot/src/agents/memory-search.ts:276` + +```typescript +// Existing kill switch - returns null when disabled +if (!resolved.enabled) return null; +``` + +**Config location:** `agents.defaults.memorySearch.enabled` (default: `true`) + +### Copy-Ready Patterns + +| Pattern | Source File | Lines | +|---------|-------------|-------| +| Minimal plugin structure | `/home/user/clawdbot/extensions/memory-core/index.ts` | 1-36 | +| Tool registration | `/home/user/clawdbot/extensions/memory-lancedb/index.ts` | 238-286 | +| Hook registration (`before_agent_start`) | `/home/user/clawdbot/extensions/memory-lancedb/index.ts` | 468-491 | +| Hook registration (`agent_end`) | `/home/user/clawdbot/extensions/memory-lancedb/index.ts` | 496-569 | +| CLI commands | `/home/user/clawdbot/extensions/memory-lancedb/index.ts` | 418-460 | +| Config schema with `parse` + `uiHints` | `/home/user/clawdbot/extensions/memory-lancedb/config.ts` | 61-114 | +| Plugin manifest | `/home/user/clawdbot/extensions/memory-lancedb/clawdbot.plugin.json` | 1-67 | + +### Anti-Patterns to Avoid + +1. **Do NOT invent API endpoints** - Only use confirmed endpoints from Phase 0 +2. **Do NOT use `Type.Union` in tool schemas** - Use `stringEnum`/`optionalStringEnum` instead +3. **Do NOT use raw `format` property** - Reserved keyword in some validators +4. **Do NOT add dependencies to root package.json** - Plugin deps go in plugin's package.json +5. **Do NOT use `workspace:*` in dependencies** - Use in `devDependencies` or `peerDependencies` only + +--- + +## Phase 1: Disable Built-in Memory + +### What to Implement + +1. Add `memorySearch.enabled: false` config to disable built-in memory tools +2. User configures this in their config file when using claude-mem + +**No code changes required.** The existing kill switch at `src/agents/memory-search.ts:276` already returns `null` when `enabled: false`. + +### Documentation References + +- Config schema: `/home/user/clawdbot/src/config/types.tools.ts:223` (`enabled?: boolean`) +- Kill switch: `/home/user/clawdbot/src/agents/memory-search.ts:276` +- Tool creation guard: `/home/user/clawdbot/src/agents/tools/memory-tool.ts:32` + +### Verification Checklist + +```bash +# 1. Set config +clawdbot config set agents.defaults.memorySearch.enabled false + +# 2. Verify memory tools are not registered +clawdbot agent --dry-run 2>&1 | grep -c "memory_search" +# Expected: 0 + +# 3. Verify config is persisted +clawdbot config get agents.defaults.memorySearch.enabled +# Expected: false +``` + +### Anti-Pattern Guards + +- Do NOT modify `resolveMemorySearchConfig()` - existing logic sufficient +- Do NOT add new config keys - reuse existing `enabled` flag + +--- + +## Phase 2: Create Plugin Structure + +### What to Implement + +Create `extensions/memory-claudemem/` by copying structure from `extensions/memory-lancedb/`. + +### Documentation References + +- Copy from: `/home/user/clawdbot/extensions/memory-lancedb/` (directory structure) +- Manifest spec: `/home/user/clawdbot/docs/plugins/manifest.md:16-42` + +### Files to Create + +``` +extensions/memory-claudemem/ +├── package.json # Copy from memory-lancedb/package.json +├── clawdbot.plugin.json # Copy from memory-lancedb/clawdbot.plugin.json +├── index.ts # Plugin entry point +├── config.ts # Config schema +├── client.ts # HTTP client for worker +└── types.ts # Type definitions +``` + +### package.json (Copy and Modify) + +**Copy from:** `/home/user/clawdbot/extensions/memory-lancedb/package.json` + +```json +{ + "name": "@clawdbot/memory-claudemem", + "version": "0.0.1", + "type": "module", + "main": "index.ts", + "dependencies": {}, + "devDependencies": { + "clawdbot": "workspace:*" + }, + "peerDependencies": { + "clawdbot": "*" + } +} +``` + +### clawdbot.plugin.json (Copy and Modify) + +**Copy from:** `/home/user/clawdbot/extensions/memory-lancedb/clawdbot.plugin.json:1-20` + +```json +{ + "id": "memory-claudemem", + "name": "Memory (Claude-Mem)", + "description": "Real-time observation and memory via claude-mem worker", + "kind": "memory", + "version": "0.0.1", + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "workerUrl": { "type": "string", "default": "http://localhost:37777" }, + "workerTimeout": { "type": "number", "default": 10000 } + } + } +} +``` + +### Verification Checklist + +```bash +# 1. Verify plugin loads +cd extensions/memory-claudemem && pnpm install + +# 2. Verify manifest is valid JSON +cat clawdbot.plugin.json | jq . + +# 3. Verify plugin appears in list +clawdbot plugins list | grep memory-claudemem +``` + +### Anti-Pattern Guards + +- Do NOT add `workspace:*` to `dependencies` (breaks `npm install`) +- Do NOT add plugin deps to root `package.json` + +--- + +## Phase 3: HTTP Client + +### What to Implement + +Create `client.ts` with methods for claude-mem worker API. + +### Documentation References + +- Observation endpoint: `POST /api/sessions/observations` (GitHub Issue #348) +- Search endpoint: `GET /api/search?query=&format=index` +- Batch fetch: `POST /api/observations/batch` (v7.3.0+) +- Health: `GET /api/health` + +### client.ts + +```typescript +// Copy timeout + AbortController pattern from: +// /home/user/clawdbot/src/infra/fetch-with-timeout.ts (if exists) +// Otherwise use standard fetch with AbortController + +export class ClaudeMemClient { + constructor(private baseUrl: string, private timeout: number) {} + + async ping(): Promise { + try { + const res = await fetch(`${this.baseUrl}/api/health`, { + signal: AbortSignal.timeout(this.timeout) + }); + return res.ok; + } catch { + return false; + } + } + + async observe(sessionId: string, toolName: string, toolInput: unknown, toolResponse: unknown): Promise { + await fetch(`${this.baseUrl}/api/sessions/observations`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + claudeSessionId: sessionId, + tool_name: toolName, + tool_input: toolInput, + tool_response: toolResponse + }), + signal: AbortSignal.timeout(this.timeout) + }); + } + + async search(query: string, limit = 10): Promise { + const params = new URLSearchParams({ + query, + type: "observations", + format: "index", + limit: String(limit) + }); + const res = await fetch(`${this.baseUrl}/api/search?${params}`, { + signal: AbortSignal.timeout(this.timeout) + }); + return res.json(); + } + + async getObservations(ids: number[]): Promise { + const res = await fetch(`${this.baseUrl}/api/observations/batch`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ids }), + signal: AbortSignal.timeout(this.timeout) + }); + return res.json(); + } +} +``` + +### Verification Checklist + +```bash +# 1. Start claude-mem worker +claude-mem worker & + +# 2. Test health endpoint +curl http://localhost:37777/api/health +# Expected: 200 OK + +# 3. Test search endpoint +curl "http://localhost:37777/api/search?query=test&format=index&limit=5" +# Expected: JSON array +``` + +### Anti-Pattern Guards + +- Do NOT invent endpoints like `/api/observe` - use documented `/api/sessions/observations` +- Do NOT assume `created_at_epoch` is in seconds - it's MILLISECONDS +- Do NOT skip error handling - worker may be offline + +--- + +## Phase 4: Hook Registration + +### What to Implement + +Register hooks that forward events to claude-mem worker. + +### Documentation References + +- Hook handler signature: `/home/user/clawdbot/src/plugins/types.ts:466-520` +- Example `before_agent_start`: `/home/user/clawdbot/extensions/memory-lancedb/index.ts:468-491` +- Example `agent_end`: `/home/user/clawdbot/extensions/memory-lancedb/index.ts:496-569` + +### Hook: after_tool_call → PostToolUse + +**Copy pattern from:** `/home/user/clawdbot/extensions/memory-lancedb/index.ts:496-520` + +```typescript +// In index.ts register() function +api.on("after_tool_call", async (event, ctx) => { + // Skip our own tools to prevent recursion + if (event.toolName.startsWith("memory_")) return; + + try { + await client.observe( + ctx.sessionId, + event.toolName, + event.params, + event.result + ); + } catch (err) { + api.logger.warn?.(`claude-mem: observation failed: ${err}`); + } +}); +``` + +### Hook: before_agent_start → Context Injection + +**Copy pattern from:** `/home/user/clawdbot/extensions/memory-lancedb/index.ts:468-491` + +```typescript +api.on("before_agent_start", async (event, ctx) => { + try { + const results = await client.search(event.prompt, 5); + if (results.length === 0) return; + + const context = results + .map(r => `- [#${r.id}] ${r.title}: ${r.snippet}`) + .join("\n"); + + return { + prependContext: `\n${context}\n` + }; + } catch (err) { + api.logger.warn?.(`claude-mem: context injection failed: ${err}`); + } +}); +``` + +### Verification Checklist + +```bash +# 1. Enable debug logging +clawdbot config set plugins.memory-claudemem.debug true + +# 2. Run a test message +clawdbot message send "test message" + +# 3. Check logs for observation +tail -f ~/.clawdbot/logs/gateway.log | grep claude-mem + +# 4. Verify in claude-mem UI +open http://localhost:37777 +``` + +### Anti-Pattern Guards + +- Do NOT observe `memory_*` tools (causes recursion) +- Do NOT block on observation failures (use fire-and-forget pattern for void hooks) +- Do NOT modify event object for void hooks (only `before_agent_start` can return modifications) + +--- + +## Phase 5: Progressive Disclosure Tools + +### What to Implement + +Register three tools following claude-mem's 3-layer retrieval pattern. + +### Documentation References + +- Tool registration: `/home/user/clawdbot/extensions/memory-lancedb/index.ts:238-286` +- Tool factory pattern: `/home/user/clawdbot/src/plugins/types.ts:69-77` +- Tool schema guardrails: Use `Type.Object` with `Type.String`, `Type.Optional`, `Type.Number` + +### Layer 1: memory_search (~50-100 tokens per result) + +**Copy pattern from:** `/home/user/clawdbot/extensions/memory-lancedb/index.ts:238-270` + +```typescript +api.registerTool({ + name: "memory_search", + label: "Memory Search", + description: "Search past observations. Returns compact results with IDs. Use memory_observations for full details.", + parameters: Type.Object({ + query: Type.String({ description: "Natural language search query" }), + limit: Type.Optional(Type.Number({ description: "Max results (default: 10)" })) + }), + async execute(_toolCallId, params) { + const results = await client.search(params.query, params.limit ?? 10); + const text = results.length === 0 + ? "No memories found." + : results.map((r, i) => `${i + 1}. [#${r.id}] ${r.title}`).join("\n"); + return { content: [{ type: "text", text }] }; + } +}, { name: "memory_search" }); +``` + +### Layer 3: memory_observations (~500-1000 tokens per result) + +```typescript +api.registerTool({ + name: "memory_observations", + label: "Memory Observations", + description: "Get full details for specific observation IDs. Use after memory_search to filter.", + parameters: Type.Object({ + ids: Type.Array(Type.Number(), { description: "Observation IDs from memory_search" }) + }), + async execute(_toolCallId, params) { + const observations = await client.getObservations(params.ids); + const text = observations.map(o => + `## #${o.id}\n${o.narrative}\n\nFiles: ${o.files_modified?.join(", ") || "none"}` + ).join("\n\n---\n\n"); + return { content: [{ type: "text", text }] }; + } +}, { name: "memory_observations" }); +``` + +### Verification Checklist + +```bash +# 1. Verify tools are registered +clawdbot tools list | grep memory_ + +# 2. Test search tool +clawdbot agent --message "Use memory_search to find recent file edits" + +# 3. Test observations tool +clawdbot agent --message "Use memory_observations with IDs [1, 2, 3]" +``` + +### Anti-Pattern Guards + +- Do NOT use `Type.Union` in parameters - not allowed by schema guardrails +- Do NOT use raw `format` property name - reserved keyword +- Do NOT return tool results without `content: [{ type: "text", text }]` structure + +--- + +## Phase 6: CLI Commands + +### What to Implement + +Add CLI commands for worker status and manual search. + +### Documentation References + +- CLI registration: `/home/user/clawdbot/extensions/memory-lancedb/index.ts:418-460` +- Commander.js pattern: `/home/user/clawdbot/src/plugins/types.ts:193-200` + +### Implementation + +**Copy pattern from:** `/home/user/clawdbot/extensions/memory-lancedb/index.ts:418-460` + +```typescript +api.registerCli(({ program }) => { + const mem = program + .command("claude-mem") + .description("Claude-mem integration commands"); + + mem.command("status") + .description("Check claude-mem worker status") + .action(async () => { + const alive = await client.ping(); + console.log(alive + ? `✓ Worker running at ${cfg.workerUrl}` + : `✗ Worker not responding at ${cfg.workerUrl}`); + }); + + mem.command("search") + .description("Search memories") + .argument("", "Search query") + .action(async (query) => { + const results = await client.search(query); + console.log(JSON.stringify(results, null, 2)); + }); +}, { commands: ["claude-mem"] }); +``` + +### Verification Checklist + +```bash +# 1. Verify command appears in help +clawdbot claude-mem --help + +# 2. Test status command +clawdbot claude-mem status + +# 3. Test search command +clawdbot claude-mem search "authentication" +``` + +### Anti-Pattern Guards + +- Do NOT use reserved command names (help, send, config, status, etc.) - see `/home/user/clawdbot/src/plugins/commands.ts:33-69` + +--- + +## Phase 7: Verification + +### Implementation Verification + +1. **Memory disabled when configured:** + ```bash + clawdbot config set agents.defaults.memorySearch.enabled false + clawdbot tools list | grep -v memory_claudemem | grep memory_ + # Expected: no results (built-in memory tools gone) + ``` + +2. **Plugin loads successfully:** + ```bash + clawdbot plugins list | grep memory-claudemem + # Expected: memory-claudemem (enabled) + ``` + +3. **Worker connection verified:** + ```bash + clawdbot claude-mem status + # Expected: ✓ Worker running at http://localhost:37777 + ``` + +4. **Observations recorded:** + ```bash + # Run a command + clawdbot agent --message "List files in current directory" + + # Check worker UI + open http://localhost:37777 + # Expected: New observation for "bash" tool + ``` + +5. **Context injection works:** + ```bash + # First, create some observations + clawdbot agent --message "Create a file called test.txt" + + # Then search should inject context + DEBUG=claude-mem clawdbot agent --message "What files did I create recently?" + # Expected: Logs show context injection + ``` + +### Anti-Pattern Check + +```bash +# Grep for known bad patterns +grep -r "Type\.Union" extensions/memory-claudemem/ +# Expected: no results + +grep -r "format:" extensions/memory-claudemem/*.ts +# Expected: no results (or only in non-schema contexts) + +grep -r "/api/observe" extensions/memory-claudemem/ +# Expected: no results (should use /api/sessions/observations) +``` + +### Test Suite + +```bash +cd extensions/memory-claudemem +pnpm test +# Expected: All tests pass + +pnpm lint +# Expected: No lint errors + +pnpm build +# Expected: Builds without type errors +``` + +--- + +## Configuration Summary + +### User Config (to use claude-mem) + +```yaml +# ~/.clawdbot/config.yml + +# Disable built-in memory +agents: + defaults: + memorySearch: + enabled: false + +# Enable claude-mem plugin +plugins: + memory-claudemem: + workerUrl: http://localhost:37777 + workerTimeout: 10000 +``` + +### Rollback (to restore built-in memory) + +```yaml +agents: + defaults: + memorySearch: + enabled: true + +plugins: + memory-claudemem: + enabled: false +``` diff --git a/extensions/memory-claudemem/clawdbot.plugin.json b/extensions/memory-claudemem/clawdbot.plugin.json new file mode 100644 index 000000000..5149fd97a --- /dev/null +++ b/extensions/memory-claudemem/clawdbot.plugin.json @@ -0,0 +1,27 @@ +{ + "id": "memory-claudemem", + "name": "Memory (Claude-Mem)", + "description": "Real-time observation and memory via claude-mem worker", + "kind": "memory", + "version": "0.0.1", + "uiHints": { + "workerUrl": { + "label": "Worker URL", + "placeholder": "http://localhost:37777", + "help": "URL of the claude-mem worker" + }, + "workerTimeout": { + "label": "Timeout (ms)", + "placeholder": "10000", + "advanced": true + } + }, + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "workerUrl": { "type": "string", "default": "http://localhost:37777" }, + "workerTimeout": { "type": "number", "default": 10000 } + } + } +} diff --git a/extensions/memory-claudemem/client.ts b/extensions/memory-claudemem/client.ts new file mode 100644 index 000000000..806a969f6 --- /dev/null +++ b/extensions/memory-claudemem/client.ts @@ -0,0 +1,143 @@ +import type { SearchResult, Observation } from "./types.js"; + +export class ClaudeMemClient { + constructor( + private readonly baseUrl: string, + private readonly timeout: number, + ) {} + + /** + * Check if the claude-mem worker is healthy. + * Returns true if the worker responds to health check, false otherwise. + */ + async ping(): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/health`, { + method: "GET", + signal: AbortSignal.timeout(this.timeout), + }); + return response.ok; + } catch { + return false; + } + } + + /** + * Record an observation to the claude-mem worker. + * Fire-and-forget pattern: errors are logged but do not block. + */ + async observe( + claudeSessionId: string, + toolName: string, + toolInput: unknown, + toolResponse: unknown, + cwd?: string, + ): Promise { + try { + const body = { + claudeSessionId, + tool_name: toolName, + tool_input: toolInput, + tool_response: toolResponse, + ...(cwd && { cwd }), + }; + + await fetch(`${this.baseUrl}/api/sessions/observations`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(this.timeout), + }); + // Fire-and-forget: we don't check response status or throw on errors + } catch { + // Silently ignore errors - worker may be offline + // This is intentional: observations should never block the main flow + } + } + + /** + * Search observations by query. + * Returns an array of search results with id, title, snippet, and score. + */ + async search(query: string, limit = 10): Promise { + try { + const searchParams = new URLSearchParams({ + query, + type: "observations", + format: "index", + limit: String(limit), + }); + + const response = await fetch( + `${this.baseUrl}/api/search?${searchParams.toString()}`, + { + method: "GET", + signal: AbortSignal.timeout(this.timeout), + }, + ); + + if (!response.ok) { + return []; + } + + const data = await response.json(); + + // The API returns results in an array format + if (Array.isArray(data)) { + return data as SearchResult[]; + } + + // Handle wrapped response format { results: [...] } + if (data && Array.isArray(data.results)) { + return data.results as SearchResult[]; + } + + return []; + } catch { + return []; + } + } + + /** + * Fetch full observation details by IDs. + * Returns observations with narrative, files_modified, tool_name, tool_input, tool_response, and created_at_epoch. + */ + async getObservations(ids: number[]): Promise { + if (ids.length === 0) { + return []; + } + + try { + const response = await fetch(`${this.baseUrl}/api/observations/batch`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ids }), + signal: AbortSignal.timeout(this.timeout), + }); + + if (!response.ok) { + return []; + } + + const data = await response.json(); + + // Handle direct array response + if (Array.isArray(data)) { + return data as Observation[]; + } + + // Handle wrapped response format { observations: [...] } + if (data && Array.isArray(data.observations)) { + return data.observations as Observation[]; + } + + return []; + } catch { + return []; + } + } +} diff --git a/extensions/memory-claudemem/config.ts b/extensions/memory-claudemem/config.ts new file mode 100644 index 000000000..b81167113 --- /dev/null +++ b/extensions/memory-claudemem/config.ts @@ -0,0 +1,49 @@ +export type ClaudeMemConfig = { + workerUrl: string; + workerTimeout: number; +}; + +const DEFAULT_WORKER_URL = "http://localhost:37777"; +const DEFAULT_TIMEOUT = 10000; + +function assertAllowedKeys( + value: Record, + allowed: string[], + label: string, +) { + const unknown = Object.keys(value).filter((key) => !allowed.includes(key)); + if (unknown.length === 0) return; + throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`); +} + +export const claudeMemConfigSchema = { + parse(value: unknown): ClaudeMemConfig { + if (!value || typeof value !== "object" || Array.isArray(value)) { + // Return defaults if no config provided + return { + workerUrl: DEFAULT_WORKER_URL, + workerTimeout: DEFAULT_TIMEOUT, + }; + } + const cfg = value as Record; + assertAllowedKeys(cfg, ["workerUrl", "workerTimeout"], "claude-mem config"); + + return { + workerUrl: typeof cfg.workerUrl === "string" ? cfg.workerUrl : DEFAULT_WORKER_URL, + workerTimeout: typeof cfg.workerTimeout === "number" ? cfg.workerTimeout : DEFAULT_TIMEOUT, + }; + }, + uiHints: { + workerUrl: { + label: "Worker URL", + placeholder: DEFAULT_WORKER_URL, + help: "URL of the claude-mem worker", + }, + workerTimeout: { + label: "Timeout (ms)", + placeholder: String(DEFAULT_TIMEOUT), + advanced: true, + help: "Request timeout in milliseconds", + }, + }, +}; diff --git a/extensions/memory-claudemem/index.ts b/extensions/memory-claudemem/index.ts new file mode 100644 index 000000000..6c7489752 --- /dev/null +++ b/extensions/memory-claudemem/index.ts @@ -0,0 +1,195 @@ +import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk"; +import { Type } from "@sinclair/typebox"; +import { claudeMemConfigSchema } from "./config.js"; +import { ClaudeMemClient } from "./client.js"; + +const claudeMemPlugin = { + id: "memory-claudemem", + name: "Memory (Claude-Mem)", + description: "Real-time observation and memory via claude-mem worker", + kind: "memory" as const, + configSchema: claudeMemConfigSchema, + + register(api: ClawdbotPluginApi) { + const cfg = claudeMemConfigSchema.parse(api.pluginConfig); + const client = new ClaudeMemClient(cfg.workerUrl, cfg.workerTimeout); + + api.logger.info( + `memory-claudemem: plugin registered (worker: ${cfg.workerUrl})`, + ); + + // Hook: after_tool_call → Observe tool calls (fire-and-forget) + api.on("after_tool_call", async (event, ctx) => { + // Skip memory tools to prevent recursion + if (event.toolName.startsWith("memory_")) return; + + try { + // Fire-and-forget: don't await, let it run in parallel + // Use sessionKey as the session identifier (may be undefined) + client.observe( + ctx.sessionKey ?? "unknown", + event.toolName, + event.params, + event.result, + ); + } catch (err) { + api.logger.warn?.(`memory-claudemem: observation failed: ${err}`); + } + }); + + // Hook: before_agent_start → Context injection from memory search + api.on("before_agent_start", async (event) => { + // Skip if prompt is empty or too short + if (!event.prompt || event.prompt.length < 5) return; + + try { + const results = await client.search(event.prompt, 5); + if (results.length === 0) return; + + const memoryContext = results + .map((r) => `- [#${r.id}] ${r.title}: ${r.snippet}`) + .join("\n"); + + api.logger.info?.( + `memory-claudemem: injecting ${results.length} memories into context`, + ); + + return { + prependContext: `\nThe following memories may be relevant:\n${memoryContext}\n`, + }; + } catch (err) { + api.logger.warn?.(`memory-claudemem: context injection failed: ${err}`); + } + }); + + // Phase 5: Tool registration + // memory_search - Layer 1: compact results (~50-100 tokens per result) + api.registerTool( + { + name: "memory_search", + label: "Memory Search", + description: + "Search past observations. Returns compact results with IDs. Use memory_observations for full details.", + parameters: Type.Object({ + query: Type.String({ description: "Search query" }), + limit: Type.Optional( + Type.Number({ description: "Max results (default: 10)" }), + ), + }), + async execute(_toolCallId, params) { + const { query, limit = 10 } = params as { + query: string; + limit?: number; + }; + + const results = await client.search(query, limit); + + if (results.length === 0) { + return { + content: [{ type: "text", text: "No relevant memories found." }], + }; + } + + const text = results.map((r) => `[#${r.id}] ${r.title}`).join("\n"); + + return { + content: [ + { + type: "text", + text: `Found ${results.length} memories:\n\n${text}`, + }, + ], + }; + }, + }, + { name: "memory_search" }, + ); + + // memory_observations - Layer 3: full details (~500-1000 tokens per result) + api.registerTool( + { + name: "memory_observations", + label: "Memory Observations", + description: + "Get full details for specific observation IDs. Use after memory_search to filter.", + parameters: Type.Object({ + ids: Type.Array(Type.Number(), { + description: "Array of observation IDs to fetch", + }), + }), + async execute(_toolCallId, params) { + const { ids } = params as { ids: number[] }; + + if (ids.length === 0) { + return { + content: [{ type: "text", text: "No observation IDs provided." }], + }; + } + + const observations = await client.getObservations(ids); + + if (observations.length === 0) { + return { + content: [ + { + type: "text", + text: "No observations found for the given IDs.", + }, + ], + }; + } + + const text = observations + .map((obs) => { + const filesSection = + obs.files_modified && obs.files_modified.length > 0 + ? `\n\nFiles: ${obs.files_modified.join(", ")}` + : ""; + return `## #${obs.id}\n${obs.narrative}${filesSection}`; + }) + .join("\n\n---\n\n"); + + return { + content: [{ type: "text", text }], + }; + }, + }, + { name: "memory_observations" }, + ); + + // Phase 6: CLI registration + api.registerCli( + ({ program }) => { + const claudeMem = program + .command("claude-mem") + .description("Claude-mem memory plugin commands"); + + claudeMem + .command("status") + .description("Check if the claude-mem worker is responding") + .action(async () => { + const isHealthy = await client.ping(); + if (isHealthy) { + console.log(`✓ Worker running at ${cfg.workerUrl}`); + } else { + console.log(`✗ Worker not responding at ${cfg.workerUrl}`); + process.exitCode = 1; + } + }); + + claudeMem + .command("search") + .description("Search memories") + .argument("", "Search query") + .option("--limit ", "Max results", "10") + .action(async (query, opts) => { + const results = await client.search(query, parseInt(opts.limit)); + console.log(JSON.stringify(results, null, 2)); + }); + }, + { commands: ["claude-mem"] }, + ); + }, +}; + +export default claudeMemPlugin; diff --git a/extensions/memory-claudemem/package.json b/extensions/memory-claudemem/package.json new file mode 100644 index 000000000..1252813b3 --- /dev/null +++ b/extensions/memory-claudemem/package.json @@ -0,0 +1,13 @@ +{ + "name": "@clawdbot/memory-claudemem", + "version": "0.0.1", + "type": "module", + "main": "index.ts", + "dependencies": {}, + "devDependencies": { + "clawdbot": "workspace:*" + }, + "peerDependencies": { + "clawdbot": "*" + } +} diff --git a/extensions/memory-claudemem/types.ts b/extensions/memory-claudemem/types.ts new file mode 100644 index 000000000..0a0833ae7 --- /dev/null +++ b/extensions/memory-claudemem/types.ts @@ -0,0 +1,24 @@ +/** Search result from claude-mem worker (index format) */ +export type SearchResult = { + id: number; + title: string; + snippet?: string; + score?: number; +}; + +/** Full observation from claude-mem worker */ +export type Observation = { + id: number; + narrative: string; + files_modified?: string[]; + tool_name?: string; + tool_input?: unknown; + tool_response?: unknown; + created_at_epoch?: number; +}; + +/** Plugin configuration */ +export type ClaudeMemConfig = { + workerUrl: string; + workerTimeout: number; +}; diff --git a/package.json b/package.json index 04322f3af..f6d67ab67 100644 --- a/package.json +++ b/package.json @@ -283,5 +283,10 @@ "apps/macos/.build/**", "dist/Moltbot.app/**" ] - } + }, + "trustedDependencies": [ + "@whiskeysockets/baileys", + "node-llama-cpp", + "protobufjs" + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c0f99928..37b37bd0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -261,6 +261,13 @@ importers: node-llama-cpp: specifier: 3.15.0 version: 3.15.0(typescript@5.9.3) + optionalDependencies: + '@napi-rs/canvas': + specifier: ^0.1.88 + version: 0.1.88 + node-llama-cpp: + specifier: 3.15.0 + version: 3.15.0(typescript@5.9.3) extensions/bluebubbles: {} @@ -354,11 +361,21 @@ importers: extensions/mattermost: {} + extensions/memory-claudemem: + devDependencies: + clawdbot: + specifier: workspace:* + version: link:../.. + extensions/memory-core: devDependencies: moltbot: specifier: workspace:* version: link:../.. + dependencies: + clawdbot: + specifier: '>=2026.1.24' + version: 2026.1.24(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3) extensions/memory-lancedb: dependencies: @@ -1323,6 +1340,7 @@ packages: resolution: {integrity: sha512-aYrIoEG24AC+wILCL57Ius/Y4yU+xFHDPKLvmjzzN4byAjzeIGF0TC86S5RBt4Ji+dxS7yIWV5Q/gE5/fybIFQ==} engines: {node: '>= 18'} cpu: [x64, arm64] + cpu: [x64, arm64] os: [darwin, linux, win32] peerDependencies: apache-arrow: '>=15.0.0 <=18.1.0' @@ -3219,6 +3237,11 @@ packages: engines: {node: '>=22.12.0'} hasBin: true + clawdbot@2026.1.24: + resolution: {integrity: sha512-foszbNXzk743kQBx2Nfc2KNlStZyBAVAYLQJ+KaONh009r8oB1a74kQ8wTnKX+SDNaQ9MvaE9tOisVUi+H3F+Q==} + engines: {node: '>=22.12.0'} + hasBin: true + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -9176,6 +9199,84 @@ snapshots: - typescript - utf-8-validate + clawdbot@2026.1.24(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3): + dependencies: + '@agentclientprotocol/sdk': 0.13.1(zod@4.3.6) + '@aws-sdk/client-bedrock': 3.975.0 + '@buape/carbon': 0.14.0(hono@4.11.4) + '@clack/prompts': 0.11.0 + '@grammyjs/runner': 2.0.3(grammy@1.39.3) + '@grammyjs/transformer-throttler': 1.2.1(grammy@1.39.3) + '@homebridge/ciao': 1.3.4 + '@line/bot-sdk': 10.6.0 + '@lydell/node-pty': 1.2.0-beta.3 + '@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-tui': 0.49.3 + '@mozilla/readability': 0.6.0 + '@sinclair/typebox': 0.34.47 + '@slack/bolt': 4.6.0(@types/express@5.0.6) + '@slack/web-api': 7.13.0 + '@whiskeysockets/baileys': 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5) + ajv: 8.17.1 + body-parser: 2.2.2 + chalk: 5.6.2 + chokidar: 5.0.0 + chromium-bidi: 13.0.1(devtools-protocol@0.0.1561482) + cli-highlight: 2.1.11 + commander: 14.0.2 + croner: 9.1.0 + detect-libc: 2.1.2 + discord-api-types: 0.38.37 + dotenv: 17.2.3 + express: 5.2.1 + file-type: 21.3.0 + grammy: 1.39.3 + hono: 4.11.4 + jiti: 2.6.1 + json5: 2.2.3 + jszip: 3.10.1 + linkedom: 0.18.12 + long: 5.3.2 + markdown-it: 14.1.0 + node-edge-tts: 1.2.9 + osc-progress: 0.3.0 + pdfjs-dist: 5.4.530 + playwright-core: 1.58.0 + proper-lockfile: 4.1.2 + qrcode-terminal: 0.12.0 + sharp: 0.34.5 + sqlite-vec: 0.1.7-alpha.2 + tar: 7.5.4 + tslog: 4.10.2 + undici: 7.19.0 + ws: 8.19.0 + yaml: 2.8.2 + zod: 4.3.6 + optionalDependencies: + '@napi-rs/canvas': 0.1.88 + node-llama-cpp: 3.15.0(typescript@5.9.3) + transitivePeerDependencies: + - '@discordjs/opus' + - '@modelcontextprotocol/sdk' + - '@types/express' + - audio-decode + - aws-crt + - bufferutil + - canvas + - debug + - devtools-protocol + - encoding + - ffmpeg-static + - jimp + - link-preview-js + - node-opus + - opusscript + - supports-color + - typescript + - utf-8-validate + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0