feat: moltbot integration (#216)

* feat: moltbot integration

* fixes

* fixes
This commit is contained in:
Nicolò Boschi 2026-01-29 16:58:04 +01:00 committed by GitHub
parent c16ccc2c22
commit 12e9a3d305
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 3206 additions and 1 deletions

View file

@ -139,6 +139,55 @@ jobs:
path: hindsight-clients/typescript/*.tgz
retention-days: 1
release-moltbot-integration:
runs-on: ubuntu-latest
environment: npm
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
working-directory: ./hindsight-integrations/moltbot
run: npm ci
- name: Build
working-directory: ./hindsight-integrations/moltbot
run: npm run build
- name: Publish to npm
working-directory: ./hindsight-integrations/moltbot
run: |
set +e
OUTPUT=$(npm publish --access public 2>&1)
EXIT_CODE=$?
echo "$OUTPUT"
if [ $EXIT_CODE -ne 0 ]; then
if echo "$OUTPUT" | grep -q "cannot publish over"; then
echo "Package version already published, skipping..."
exit 0
fi
exit $EXIT_CODE
fi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Pack for GitHub release
working-directory: ./hindsight-integrations/moltbot
run: npm pack
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: moltbot-integration
path: hindsight-integrations/moltbot/*.tgz
retention-days: 1
release-control-plane:
runs-on: ubuntu-latest
environment: npm
@ -366,7 +415,7 @@ jobs:
create-github-release:
runs-on: ubuntu-latest
needs: [release-python-packages, release-typescript-client, release-control-plane, release-rust-cli, release-docker-images, release-helm-chart]
needs: [release-python-packages, release-typescript-client, release-moltbot-integration, release-control-plane, release-rust-cli, release-docker-images, release-helm-chart]
permissions:
contents: write
@ -389,6 +438,12 @@ jobs:
name: typescript-client
path: ./artifacts/typescript-client
- name: Download Moltbot Integration
uses: actions/download-artifact@v4
with:
name: moltbot-integration
path: ./artifacts/moltbot-integration
- name: Download Control Plane
uses: actions/download-artifact@v4
with:
@ -430,6 +485,8 @@ jobs:
cp artifacts/python-packages/hindsight-embed/dist/* release-assets/ || true
# TypeScript client
cp artifacts/typescript-client/*.tgz release-assets/ || true
# Moltbot Integration
cp artifacts/moltbot-integration/*.tgz release-assets/ || true
# Control Plane
cp artifacts/control-plane/*.tgz release-assets/ || true
# Rust CLI binaries

View file

@ -82,6 +82,29 @@ jobs:
- name: Build TypeScript client
run: npm run build --workspace=hindsight-clients/typescript
build-moltbot-integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies
working-directory: ./hindsight-integrations/moltbot
run: npm ci
- name: Run tests
working-directory: ./hindsight-integrations/moltbot
run: npm test
- name: Build
working-directory: ./hindsight-integrations/moltbot
run: npm run build
build-control-plane:
runs-on: ubuntu-latest

View file

@ -0,0 +1,257 @@
---
sidebar_position: 4
---
# Moltbot (Clawdbot)
Biomimetic long-term memory for [Moltbot](https://molt.bot) using [Hindsight](https://vectorize.io/hindsight). Automatically captures conversations and intelligently recalls relevant context.
## Quick Start
```bash
# 1. Install the plugin
npm install -g @vectorize-io/hindsight-moltbot-plugin
# 2. Configure your LLM provider
export OPENAI_API_KEY="sk-your-key"
clawdbot config set 'agents.defaults.models."openai/gpt-4o-mini"' '{}'
# 3. Enable the plugin
clawdbot plugins enable hindsight-memory
# 4. Start Moltbot
clawdbot gateway
```
That's it! The plugin will automatically start capturing and recalling memories.
## How It Works
### Auto-Capture (Hooks)
Every conversation is **automatically stored** after each turn:
- Extracts facts, entities, and relationships
- Processes in background (non-blocking)
- Stores in PostgreSQL via embedded `hindsight-api`
### Auto-Recall (Before Agent Start)
Before each agent response, relevant memories are **automatically injected**:
- Relevant memories retrieved (up to 1024 tokens)
- Injected into context with `<hindsight-context>` tags
- Agent seamlessly uses past context
## Understanding Moltbot Concepts
### Plugins
Extensions that add functionality to Moltbot. This Hindsight plugin:
- Runs a background service (manages `hindsight-embed` daemon)
- Registers hooks (automatic event handlers)
### Hooks
Automatic event handlers that run without agent involvement:
- **`before_agent_start`**: Auto-recall - injects memories before agent processes message
- **`agent_end`**: Auto-capture - stores conversation after agent responds
Think of hooks as "forced automation" - they always run.
## Architecture
```
┌─────────────────────────────────────────┐
│ Moltbot Gateway │
│ │
│ ┌───────────────────────────────────┐ │
│ │ Hindsight Plugin │ │
│ │ │ │
│ │ • Service: Manages daemon │ │
│ │ • Hook: before_agent_start │ │
│ │ → Auto-recall (1024 tokens) │ │
│ │ • Hook: agent_end │ │
│ │ → Auto-capture │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
uvx hindsight-embed
• Daemon on port 8889
• PostgreSQL (pg0)
• Fact extraction
```
## Installation
### Prerequisites
- **Node.js** 22+
- **Moltbot** (Clawdbot) with plugin support
- **uv/uvx** for running `hindsight-embed`
- **LLM API key** (OpenAI, Anthropic, etc.)
### Setup
```bash
# 1. Install the plugin
npm install -g @vectorize-io/hindsight-moltbot-plugin
# 2. Configure your LLM provider
export OPENAI_API_KEY="sk-your-key"
clawdbot config set 'agents.defaults.models."openai/gpt-4o-mini"' '{}'
# 3. Enable the plugin
clawdbot plugins enable hindsight-memory
# 4. Start Moltbot
clawdbot gateway
```
On first start, `uvx` will automatically download `hindsight-embed` (no manual installation needed).
## Configuration
Optional settings in `~/.clawdbot/clawdbot.json`:
```json
{
"plugins": {
"entries": {
"hindsight-memory": {
"enabled": true,
"config": {
"daemonIdleTimeout": 0
}
}
}
}
}
```
**Options:**
- `daemonIdleTimeout` (number, default: `0`) - Seconds before daemon shuts down from inactivity (0 = never)
- `embedPort` (number, default: auto) - Port for embedded server
- `bankMission` (string, default: none) - Custom context for the memory bank
## Supported LLM Providers
The plugin auto-detects your configured provider and API key:
| Provider | Environment Variable | Model Example |
|----------|---------------------|---------------|
| OpenAI | `OPENAI_API_KEY` | `openai/gpt-4o-mini` |
| Anthropic | `ANTHROPIC_API_KEY` | `anthropic/claude-sonnet-4` |
| Gemini | `GEMINI_API_KEY` | `gemini/gemini-2.0-flash-exp` |
| Groq | `GROQ_API_KEY` | `groq/llama-3.3-70b` |
| Ollama | None needed | `ollama/llama3` |
Configure with:
```bash
export OPENAI_API_KEY="sk-your-key"
clawdbot config set 'agents.defaults.models."openai/gpt-4o-mini"' '{}'
```
## Verification
**Check if plugin is loaded:**
```bash
clawdbot plugins list | grep hindsight
# Should show: ✓ enabled │ Hindsight Memory │ ...
```
**Test auto-recall:**
Send a message on any Moltbot channel (Telegram, Slack, etc.):
```
User: My name is John and I love pizza
Bot: Got it! I'll remember that.
User: What do I like to eat?
Bot: You love pizza! # ← Used auto-recall
```
**View daemon logs:**
```bash
tail -f ~/.hindsight/daemon.log
```
**Check memories in database:**
```bash
uvx hindsight-embed memory recall moltbot "pizza" --output json
```
## Troubleshooting
**Plugin not loading?**
```bash
# Check plugin installation
npm list -g @vectorize-io/hindsight-moltbot-plugin
# Reinstall if needed
npm install -g @vectorize-io/hindsight-moltbot-plugin
clawdbot plugins enable hindsight-memory
```
**Daemon not starting?**
```bash
# Check daemon status
uvx hindsight-embed daemon status
# Manually start
uvx hindsight-embed daemon start
# View logs
tail -f ~/.hindsight/daemon.log
```
**No API key error?**
```bash
# Set in shell profile
echo 'export OPENAI_API_KEY="sk-your-key"' >> ~/.zshrc
source ~/.zshrc
# Verify
echo $OPENAI_API_KEY
```
**Memories not being stored?**
```bash
# Check gateway logs for auto-capture
tail -f /tmp/clawdbot/clawdbot-*.log | grep Hindsight
# Should see:
# [Hindsight Hook] agent_end triggered
# [Hindsight] Retained X messages for session ...
```
## Development
```bash
# Clone repo
git clone https://github.com/vectorize-io/hindsight.git
cd hindsight/hindsight-integrations/moltbot
# Install dependencies
npm install
# Build
npm run build
# Run tests
npm test
# Install locally
npm run build && ./install.sh
```
## Requirements
- **Node.js** 22+
- **Moltbot** (Clawdbot) with plugin support
- **uv/uvx** for running `hindsight-embed`
- **LLM API key** (OpenAI, Anthropic, etc.)
## License
MIT
## Links
- [Hindsight Documentation](https://vectorize.io/hindsight)
- [Moltbot Documentation](https://docs.molt.bot)
- [GitHub Repository](https://github.com/vectorize-io/hindsight)

View file

@ -187,6 +187,11 @@ const sidebars: SidebarsConfig = {
id: 'sdks/integrations/litellm',
label: 'LiteLLM',
},
{
type: 'doc',
id: 'sdks/integrations/moltbot',
label: 'Moltbot',
},
{
type: 'doc',
id: 'sdks/integrations/skills',

View file

@ -0,0 +1,4 @@
node_modules/
dist/
*.log
.DS_Store

View file

@ -0,0 +1,38 @@
# Hindsight Memory Plugin for Moltbot
Biomimetic long-term memory for [Moltbot](https://molt.bot) using [Hindsight](https://vectorize.io/hindsight). Automatically captures conversations and intelligently recalls relevant context.
## Quick Start
```bash
# 1. Install the plugin
npm install -g @vectorize-io/hindsight-moltbot-plugin
# 2. Configure your LLM provider
export OPENAI_API_KEY="sk-your-key"
clawdbot config set 'agents.defaults.models."openai/gpt-4o-mini"' '{}'
# 3. Enable the plugin
clawdbot plugins enable hindsight-memory
# 4. Start Moltbot
clawdbot gateway
```
That's it! The plugin will automatically start capturing and recalling memories.
## Documentation
For full documentation, configuration options, troubleshooting, and development guide, see:
**[Moltbot Integration Documentation](https://vectorize.io/hindsight/sdks/integrations/moltbot)**
## Links
- [Hindsight Documentation](https://vectorize.io/hindsight)
- [Moltbot Documentation](https://docs.molt.bot)
- [GitHub Repository](https://github.com/vectorize-io/hindsight)
## License
MIT

View file

@ -0,0 +1,33 @@
{
"id": "hindsight-memory",
"name": "Hindsight Memory",
"kind": "memory",
"moltbot": {
"skills": ["skills"]
},
"configSchema": {
"type": "object",
"properties": {
"bankMission": {
"type": "string",
"description": "Custom mission/context for the memory bank (overrides default)"
},
"embedPort": {
"type": "number",
"description": "Port for hindsight-embed server (auto-assigned if not specified)",
"default": 0
}
},
"additionalProperties": false
},
"uiHints": {
"bankMission": {
"label": "Bank Mission",
"placeholder": "Custom context for what this agent does..."
},
"embedPort": {
"label": "Embed Server Port",
"placeholder": "0 (auto-assign)"
}
}
}

View file

@ -0,0 +1,25 @@
---
name: hindsight-retain-messages
description: Automatically retains messages to Hindsight long-term memory
events:
- agent_end
metadata:
moltbot:
emoji: 🧠
---
# Hindsight Message Retention
This hook automatically retains conversation messages to Hindsight's long-term memory.
## When It Runs
- On `agent_end`: After each agent turn completes
## What It Does
1. Captures the current session messages
2. Formats them into a conversation transcript
3. Calls Hindsight's retain API with the session_id as document_id
4. Queues for background processing (async)
5. Extracts facts, entities, and relationships from the conversation

View file

@ -0,0 +1,68 @@
// Handler for auto-retaining messages to Hindsight
const handler = async (event) => {
console.log(`[Hindsight Hook] Received event: ${event.type}`);
// Only process agent_end events (after each agent turn)
if (event.type !== 'agent_end') {
return;
}
console.log('[Hindsight Hook] Processing retention after agent turn...');
try {
// Get client from global (set by main plugin)
const clientGlobal = global.__hindsightClient;
if (!clientGlobal) {
console.warn('[Hindsight] Client global not found, skipping retain');
return;
}
const client = clientGlobal.getClient();
if (!client) {
console.warn('[Hindsight] Client not initialized, skipping retain');
return;
}
// Extract session information
const { sessionId, sessionKey } = event.context || {};
if (!sessionId) {
return;
}
// Get messages from the event context
const sessionEntry = event.context?.sessionEntry;
if (!sessionEntry || !sessionEntry.messages || sessionEntry.messages.length === 0) {
return;
}
// Format messages into a transcript
const transcript = sessionEntry.messages
.map((msg) => {
const role = msg.role || 'unknown';
const content = msg.content || '';
return `${role}: ${content}`;
})
.join('\n\n');
if (!transcript.trim()) {
return;
}
// Retain to Hindsight with session_id as document_id
await client.retain({
content: transcript,
document_id: sessionId,
metadata: {
session_key: sessionKey,
retained_at: new Date().toISOString(),
message_count: sessionEntry.messages.length,
},
});
console.log(`[Hindsight] Retained ${sessionEntry.messages.length} messages for session ${sessionId}`);
} catch (error) {
console.error('[Hindsight] Error retaining messages:', error);
}
};
export default handler;

View file

@ -0,0 +1,71 @@
// Handler for auto-retaining messages to Hindsight
import type { HookHandler } from 'moltbot/plugin-sdk';
const handler: HookHandler = async (event) => {
// Only process tool_result_persist and command:new events
if (
event.type !== 'tool_result_persist' &&
!(event.type === 'command' && event.action === 'new')
) {
return;
}
try {
// Get client from global (set by main plugin)
const clientGlobal = (global as any).__hindsightClient;
if (!clientGlobal) {
console.warn('[Hindsight] Client global not found, skipping retain');
return;
}
const client = clientGlobal.getClient();
if (!client) {
console.warn('[Hindsight] Client not initialized, skipping retain');
return;
}
// Extract session information
const { sessionId, sessionKey } = event.context || {};
if (!sessionId) {
return;
}
// Get messages from the event context
// The messages are in event.context.sessionEntry or similar
const sessionEntry = event.context?.sessionEntry;
if (!sessionEntry || !sessionEntry.messages || sessionEntry.messages.length === 0) {
return;
}
// Format messages into a transcript
const transcript = sessionEntry.messages
.map((msg: any) => {
const role = msg.role || 'unknown';
const content = msg.content || '';
return `${role}: ${content}`;
})
.join('\n\n');
if (!transcript.trim()) {
return;
}
// Retain to Hindsight with session_id as document_id
await client.retain({
content: transcript,
document_id: sessionId,
metadata: {
session_key: sessionKey,
retained_at: new Date().toISOString(),
message_count: sessionEntry.messages.length,
},
});
console.log(`[Hindsight] Retained ${sessionEntry.messages.length} messages for session ${sessionId}`);
} catch (error) {
console.error('[Hindsight] Error retaining messages:', error);
}
};
export default handler;

View file

@ -0,0 +1,49 @@
#!/bin/bash
set -e
echo "🚀 Installing Hindsight Memory Plugin for Moltbot..."
# Get the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
INSTALL_DIR="$HOME/.clawdbot/extensions/hindsight-memory"
# Check Node version
if ! command -v node &> /dev/null; then
echo "❌ Node.js not found. Please install Node.js 22+"
exit 1
fi
# Build the plugin
echo "📦 Building plugin..."
cd "$SCRIPT_DIR"
npm install
npm run build
# Deploy to Clawdbot extensions
echo "📂 Deploying to $INSTALL_DIR..."
rm -rf "$INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
# Copy files
cp -r dist package.json clawdbot.plugin.json hooks README.md "$INSTALL_DIR/"
# Install dependencies in deployed location
echo "📥 Installing dependencies..."
cd "$INSTALL_DIR"
npm install
echo ""
echo "✅ Hindsight Memory Plugin installed successfully!"
echo ""
echo "📋 Next steps:"
echo ""
echo "1. Make sure you have an OpenAI API key set:"
echo " export OPENAI_API_KEY=\"sk-your-key-here\""
echo ""
echo "2. Enable the plugin:"
echo " clawdbot plugins enable hindsight-memory"
echo ""
echo "3. Start Moltbot:"
echo " clawdbot start"
echo ""
echo "On first start, uvx will automatically download hindsight-embed (no manual install needed)"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,58 @@
{
"name": "@vectorize-io/hindsight-moltbot-plugin",
"version": "0.1.0",
"description": "Hindsight memory plugin for Moltbot - biomimetic long-term memory with fact extraction",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"clawdbot": {
"extensions": [
"./dist/index.js"
],
"hooks": [
"hooks/retain-messages"
]
},
"keywords": [
"moltbot",
"clawdbot",
"memory",
"ai",
"agent",
"hindsight",
"long-term-memory"
],
"author": "Vectorize <support@vectorize.io>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/vectorize-io/hindsight.git",
"directory": "hindsight-integrations/moltbot"
},
"files": [
"dist",
"clawdbot.plugin.json",
"hooks",
"README.md"
],
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist",
"test": "vitest run",
"test:watch": "vitest",
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"node-fetch": "^3.3.2"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@vitest/ui": "^4.0.18",
"typescript": "^5.3.0",
"vitest": "^4.0.18"
},
"engines": {
"node": ">=22"
}
}

View file

@ -0,0 +1,34 @@
---
name: memory_search
description: Search your long-term memory for relevant facts, experiences, and context using semantic and graph-based retrieval
user-invocable: false
disable-model-invocation: false
---
# memory_search
Search your long-term memory for relevant information. This tool provides multi-strategy retrieval combining:
- Semantic search across facts and experiences
- BM25 keyword matching
- Entity graph traversal
- Temporal queries
- Cross-encoder reranking
## Usage
Call `memory_search` with a natural language query to find relevant memories:
```
memory_search "What does the user prefer for breakfast?"
memory_search "When did we discuss the project deadline?"
memory_search "Tell me about Paris"
```
## Returns
Returns a list of relevant memory fragments with:
- Content: The actual memory text
- Score: Relevance score (0-1)
- Metadata: Source document, creation date, entities
Use the results to inform your responses with context from past conversations.

View file

@ -0,0 +1,46 @@
// Handler for memory_search tool
// This will be called when the agent invokes memory_search
import { getClient } from '../../src/index.js';
export interface ToolContext {
query: string;
args: Record<string, unknown>;
}
export async function handle(ctx: ToolContext): Promise<string> {
try {
const { query } = ctx;
const client = getClient();
if (!client) {
throw new Error('Hindsight client not initialized');
}
// Call Hindsight recall API
const response = await client.recall({
query,
limit: 10,
});
// Format results for the agent
if (!response.results || response.results.length === 0) {
return 'No relevant memories found for this query.';
}
const formatted = response.results
.map((result: any, idx: number) => {
const score = result.score ? ` (relevance: ${result.score.toFixed(2)})` : '';
const date = result.metadata?.created_at
? ` [${new Date(result.metadata.created_at).toLocaleDateString()}]`
: '';
return `${idx + 1}. ${result.content}${score}${date}`;
})
.join('\n\n');
return `Found ${response.results.length} relevant memories:\n\n${formatted}`;
} catch (error) {
console.error('[Hindsight] memory_search error:', error);
return `Error searching memories: ${error instanceof Error ? error.message : String(error)}`;
}
}

View file

@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest';
import { HindsightClient } from './client.js';
describe('HindsightClient', () => {
it('should create instance with provider and API key', () => {
const client = new HindsightClient('openai', 'test-key', 'gpt-4');
expect(client).toBeInstanceOf(HindsightClient);
});
it('should set bank ID', () => {
const client = new HindsightClient('openai', 'test-key');
client.setBankId('test-bank');
// No error thrown means success
expect(true).toBe(true);
});
it('should handle content escaping for single quotes', () => {
const client = new HindsightClient('openai', 'test-key');
// This test validates the client is instantiated correctly
// Actual CLI calls would require mocking
expect(client).toBeDefined();
});
});

View file

@ -0,0 +1,92 @@
import fetch from 'node-fetch';
import { exec } from 'child_process';
import { promisify } from 'util';
import type {
RetainRequest,
RetainResponse,
RecallRequest,
RecallResponse,
} from './types.js';
const execAsync = promisify(exec);
export class HindsightClient {
private bankId: string = 'default'; // Always use default bank
private llmProvider: string;
private llmApiKey: string;
private llmModel?: string;
constructor(llmProvider: string, llmApiKey: string, llmModel?: string) {
this.llmProvider = llmProvider;
this.llmApiKey = llmApiKey;
this.llmModel = llmModel;
}
setBankId(bankId: string): void {
this.bankId = bankId;
}
private getEnv(): Record<string, string> {
const env: Record<string, string> = {
...process.env,
HINDSIGHT_EMBED_LLM_PROVIDER: this.llmProvider,
HINDSIGHT_EMBED_LLM_API_KEY: this.llmApiKey,
};
if (this.llmModel) {
env.HINDSIGHT_EMBED_LLM_MODEL = this.llmModel;
}
return env;
}
async retain(request: RetainRequest): Promise<RetainResponse> {
const content = request.content.replace(/'/g, "'\\''"); // Escape single quotes
const docId = request.document_id || 'conversation';
const cmd = `uvx hindsight-embed memory retain ${this.bankId} '${content}' --doc-id '${docId}' --async`;
try {
const { stdout } = await execAsync(cmd, { env: this.getEnv() });
console.log(`[Hindsight] Retained (async): ${stdout.trim()}`);
// Return a simple response
return {
message: 'Memory queued for background processing',
document_id: docId,
memory_unit_ids: [],
};
} catch (error) {
throw new Error(`Failed to retain memory: ${error}`);
}
}
async recall(request: RecallRequest): Promise<RecallResponse> {
const query = request.query.replace(/'/g, "'\\''"); // Escape single quotes
const maxTokens = request.max_tokens || 1024;
const cmd = `uvx hindsight-embed memory recall ${this.bankId} '${query}' --output json --max-tokens ${maxTokens}`;
try {
const { stdout } = await execAsync(cmd, { env: this.getEnv() });
// Parse JSON output - returns { entities: {...}, results: [...] }
const response = JSON.parse(stdout);
const results = response.results || [];
return {
results: results.map((r: any) => ({
content: r.text || r.content || '',
score: 1.0, // CLI doesn't return scores
metadata: {
document_id: r.document_id,
chunk_id: r.chunk_id,
...r.metadata,
},
})),
};
} catch (error) {
throw new Error(`Failed to recall memories: ${error}`);
}
}
}

View file

@ -0,0 +1,143 @@
import { spawn, ChildProcess } from 'child_process';
import { promises as fs } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { execSync } from 'child_process';
export class HindsightEmbedManager {
private process: ChildProcess | null = null;
private port: number;
private baseUrl: string;
private embedDir: string;
private llmProvider: string;
private llmApiKey: string;
private llmModel?: string;
private daemonIdleTimeout: number;
constructor(
port: number,
llmProvider: string,
llmApiKey: string,
llmModel?: string,
daemonIdleTimeout: number = 0 // Default: never timeout
) {
this.port = 8889; // hindsight-embed uses fixed port 8889
this.baseUrl = `http://127.0.0.1:8889`;
this.embedDir = join(homedir(), '.clawdbot', 'hindsight-embed');
this.llmProvider = llmProvider;
this.llmApiKey = llmApiKey;
this.llmModel = llmModel;
this.daemonIdleTimeout = daemonIdleTimeout;
}
async start(): Promise<void> {
console.log(`[Hindsight] Starting hindsight-embed daemon...`);
// Build environment variables
const env: NodeJS.ProcessEnv = {
...process.env,
HINDSIGHT_EMBED_LLM_PROVIDER: this.llmProvider,
HINDSIGHT_EMBED_LLM_API_KEY: this.llmApiKey,
HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT: this.daemonIdleTimeout.toString(),
};
if (this.llmModel) {
env['HINDSIGHT_EMBED_LLM_MODEL'] = this.llmModel;
}
// Start hindsight-embed daemon (it manages itself)
const startDaemon = spawn(
'uvx',
['hindsight-embed', 'daemon', 'start'],
{
env,
stdio: 'pipe',
}
);
// Collect output
let output = '';
startDaemon.stdout?.on('data', (data) => {
const text = data.toString();
output += text;
console.log(`[Hindsight] ${text.trim()}`);
});
startDaemon.stderr?.on('data', (data) => {
const text = data.toString();
output += text;
console.error(`[Hindsight] ${text.trim()}`);
});
// Wait for daemon start command to complete
await new Promise<void>((resolve, reject) => {
startDaemon.on('exit', (code) => {
if (code === 0) {
console.log('[Hindsight] Daemon start command completed');
resolve();
} else {
reject(new Error(`Daemon start failed with code ${code}: ${output}`));
}
});
startDaemon.on('error', (error) => {
reject(error);
});
});
// Wait for server to be ready
await this.waitForReady();
console.log('[Hindsight] Daemon is ready');
}
async stop(): Promise<void> {
console.log('[Hindsight] Stopping hindsight-embed daemon...');
const stopDaemon = spawn('uvx', ['hindsight-embed', 'daemon', 'stop'], {
stdio: 'pipe',
});
await new Promise<void>((resolve) => {
stopDaemon.on('exit', () => {
console.log('[Hindsight] Daemon stopped');
resolve();
});
stopDaemon.on('error', (error) => {
console.error('[Hindsight] Error stopping daemon:', error);
resolve(); // Resolve anyway
});
// Timeout after 5 seconds
setTimeout(() => {
console.log('[Hindsight] Daemon stop timeout');
resolve();
}, 5000);
});
}
private async waitForReady(maxAttempts = 30): Promise<void> {
console.log('[Hindsight] Waiting for daemon to be ready...');
for (let i = 0; i < maxAttempts; i++) {
try {
const response = await fetch(`${this.baseUrl}/health`);
if (response.ok) {
console.log('[Hindsight] Daemon health check passed');
return;
}
} catch {
// Not ready yet
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
throw new Error('Hindsight daemon failed to become ready within 30 seconds');
}
getBaseUrl(): string {
return this.baseUrl;
}
isRunning(): boolean {
return this.process !== null;
}
}

View file

@ -0,0 +1,361 @@
import type { MoltbotPluginAPI, PluginConfig } from './types.js';
import { HindsightEmbedManager } from './embed-manager.js';
import { HindsightClient } from './client.js';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
// Module-level state
let embedManager: HindsightEmbedManager | null = null;
let client: HindsightClient | null = null;
// Global access for hooks (Moltbot loads hooks separately)
if (typeof global !== 'undefined') {
(global as any).__hindsightClient = {
getClient: () => client,
};
}
// Get directory of current module
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Default bank name
const BANK_NAME = 'moltbot';
// Provider mapping: moltbot provider name -> hindsight provider name
const PROVIDER_MAP: Record<string, string> = {
anthropic: 'anthropic',
openai: 'openai',
'openai-codex': 'openai',
gemini: 'gemini',
groq: 'groq',
ollama: 'ollama',
};
// Environment variable mapping
const ENV_KEY_MAP: Record<string, string> = {
anthropic: 'ANTHROPIC_API_KEY',
openai: 'OPENAI_API_KEY',
'openai-codex': 'OPENAI_API_KEY',
gemini: 'GEMINI_API_KEY',
groq: 'GROQ_API_KEY',
ollama: '', // No key needed for local ollama
};
function detectLLMConfig(api: MoltbotPluginAPI): {
provider: string;
apiKey: string;
model?: string;
envKey?: string;
} {
// Get models from config (agents.defaults.models is a dictionary of models)
const models = api.config.agents?.defaults?.models;
if (!models || Object.keys(models).length === 0) {
throw new Error(
'No models configured in Moltbot. Please configure at least one model in agents.defaults.models'
);
}
// Try all configured models to find one with an available API key
const configuredModels = Object.keys(models);
for (const modelKey of configuredModels) {
const [moltbotProvider, ...modelParts] = modelKey.split('/');
const model = modelParts.join('/');
const hindsightProvider = PROVIDER_MAP[moltbotProvider];
if (!hindsightProvider) {
continue; // Skip unsupported providers
}
const envKey = ENV_KEY_MAP[moltbotProvider];
const apiKey = envKey ? process.env[envKey] || '' : '';
// For ollama, no key is needed
if (hindsightProvider === 'ollama') {
return { provider: hindsightProvider, apiKey: '', model, envKey: '' };
}
// If we found a key, use this provider
if (apiKey) {
return { provider: hindsightProvider, apiKey, model, envKey };
}
}
// No API keys found for any provider - show helpful error
const configuredProviders = configuredModels
.map(m => m.split('/')[0])
.filter(p => PROVIDER_MAP[p]);
const keyInstructions = configuredProviders
.map(p => {
const envVar = ENV_KEY_MAP[p];
return envVar ? `${envVar} (for ${p})` : null;
})
.filter(Boolean)
.join('\n');
throw new Error(
`No API keys found for Hindsight memory plugin.\n\n` +
`Configured providers in Moltbot: ${configuredProviders.join(', ')}\n\n` +
`Please set one of these environment variables:\n${keyInstructions}\n\n` +
`You can set them in your shell profile (~/.zshrc or ~/.bashrc):\n` +
` export ANTHROPIC_API_KEY="your-key-here"\n\n` +
`Or run Moltbot with the environment variable:\n` +
` ANTHROPIC_API_KEY="your-key" clawdbot start\n\n` +
`Alternatively, configure ollama provider which doesn't require an API key.`
);
}
function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
const config = api.config.plugins?.entries?.['hindsight-memory']?.config || {};
return {
bankMission: config.bankMission,
embedPort: config.embedPort || 0,
daemonIdleTimeout: config.daemonIdleTimeout !== undefined ? config.daemonIdleTimeout : 0,
};
}
export default function (api: MoltbotPluginAPI) {
try {
console.log('[Hindsight] Plugin loading...');
// Detect LLM configuration from Moltbot
console.log('[Hindsight] Detecting LLM config...');
const llmConfig = detectLLMConfig(api);
if (llmConfig.provider === 'ollama') {
console.log(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${llmConfig.model || 'default'} (no API key required)`);
} else {
console.log(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${llmConfig.model || 'default'} (API key: ${llmConfig.envKey})`);
}
console.log('[Hindsight] Getting plugin config...');
const pluginConfig = getPluginConfig(api);
if (pluginConfig.bankMission) {
console.log(`[Hindsight] Custom bank mission configured: "${pluginConfig.bankMission.substring(0, 50)}..."`);
}
console.log(`[Hindsight] Daemon idle timeout: ${pluginConfig.daemonIdleTimeout}s (0 = never timeout)`);
// Determine port
const port = pluginConfig.embedPort || Math.floor(Math.random() * 10000) + 10000;
console.log(`[Hindsight] Port: ${port}`);
// Register background service
console.log('[Hindsight] Registering service...');
api.registerService({
id: 'hindsight-memory',
async start() {
try {
console.log('[Hindsight] Service starting...');
// Initialize embed manager
console.log('[Hindsight] Creating HindsightEmbedManager...');
embedManager = new HindsightEmbedManager(
port,
llmConfig.provider,
llmConfig.apiKey,
llmConfig.model,
pluginConfig.daemonIdleTimeout
);
// Start the embedded server
console.log('[Hindsight] Starting embedded server...');
await embedManager.start();
// Initialize client
console.log('[Hindsight] Creating HindsightClient...');
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model);
// Use moltbot bank
console.log(`[Hindsight] Using bank: ${BANK_NAME}`);
client.setBankId(BANK_NAME);
console.log('[Hindsight] Service ready');
} catch (error) {
console.error('[Hindsight] Service start error:', error);
throw error;
}
},
async stop() {
try {
console.log('[Hindsight] Service stopping...');
if (embedManager) {
await embedManager.stop();
embedManager = null;
}
client = null;
console.log('[Hindsight] Service stopped');
} catch (error) {
console.error('[Hindsight] Service stop error:', error);
throw error;
}
},
});
console.log('[Hindsight] Plugin loaded successfully');
// Register agent_end hook for auto-retention
console.log('[Hindsight] Registering agent_end hook...');
// Store session key for retention
let currentSessionKey: string | undefined;
// Auto-recall: Inject relevant memories before agent processes the message
api.on('before_agent_start', async (context: any) => {
try {
// Capture session key
if (context.sessionKey) {
currentSessionKey = context.sessionKey as string;
console.log('[Hindsight] Captured session key:', currentSessionKey);
}
// Get the user's latest message for recall
let prompt = context.prompt;
if (!prompt || typeof prompt !== 'string' || prompt.length < 5) {
return; // Skip very short messages
}
// Extract actual message from Telegram format: [Telegram ... GMT+1] actual message
const telegramMatch = prompt.match(/\[Telegram[^\]]+\]\s*(.+)$/);
if (telegramMatch) {
prompt = telegramMatch[1].trim();
}
if (prompt.length < 5) {
return; // Skip very short messages after extraction
}
// Get client from global
const clientGlobal = (global as any).__hindsightClient;
if (!clientGlobal) {
return;
}
const client = clientGlobal.getClient();
if (!client) {
return;
}
console.log('[Hindsight] Auto-recall for prompt:', prompt.substring(0, 50));
// Recall relevant memories (up to 1024 tokens)
const response = await client.recall({
query: prompt,
max_tokens: 1024,
});
if (!response.results || response.results.length === 0) {
console.log('[Hindsight] No memories found for auto-recall');
return;
}
// Format memories for injection
const memories = response.results
.map((result: any, idx: number) => {
const score = result.score ? ` (relevance: ${result.score.toFixed(2)})` : '';
return `${idx + 1}. ${result.content}${score}`;
})
.join('\n\n');
const contextMessage = `<hindsight-context>
You have access to long-term memory from previous conversations. Here are relevant memories:
${memories}
Use this context naturally when relevant to the conversation. Don't mention "memory" or "recall" unless specifically asked about past conversations.
</hindsight-context>`;
console.log(`[Hindsight] Auto-recall: Injecting ${response.results.length} memories`);
// Inject context before the user message
return { prependContext: contextMessage };
} catch (error) {
console.error('[Hindsight] Auto-recall error:', error);
return;
}
});
api.on('agent_end', async (event: any) => {
try {
console.log('[Hindsight Hook] agent_end triggered');
// Check event success and messages
if (!event.success || !Array.isArray(event.messages) || event.messages.length === 0) {
console.log('[Hindsight Hook] Skipping: success:', event.success, 'messages:', event.messages?.length);
return;
}
// Get client from global
const clientGlobal = (global as any).__hindsightClient;
if (!clientGlobal) {
console.warn('[Hindsight] Client global not found, skipping retain');
return;
}
const client = clientGlobal.getClient();
if (!client) {
console.warn('[Hindsight] Client not initialized, skipping retain');
return;
}
// Format messages into a transcript
const transcript = event.messages
.map((msg: any) => {
const role = msg.role || 'unknown';
let content = '';
// Handle different content formats
if (typeof msg.content === 'string') {
content = msg.content;
} else if (Array.isArray(msg.content)) {
content = msg.content
.filter((block: any) => block.type === 'text')
.map((block: any) => block.text)
.join('\n');
}
return `[role: ${role}]\n${content}\n[${role}:end]`;
})
.join('\n\n');
if (!transcript.trim() || transcript.length < 10) {
console.log('[Hindsight Hook] Transcript too short, skipping');
return;
}
// Use session key as document ID
const documentId = currentSessionKey || 'default-session';
// Retain to Hindsight
await client.retain({
content: transcript,
document_id: documentId,
metadata: {
retained_at: new Date().toISOString(),
message_count: event.messages.length,
},
});
console.log(`[Hindsight] Retained ${event.messages.length} messages for session ${documentId}`);
} catch (error) {
console.error('[Hindsight] Error retaining messages:', error);
}
});
console.log('[Hindsight] Hook registered');
} catch (error) {
console.error('[Hindsight] Plugin loading error:', error);
if (error instanceof Error) {
console.error('[Hindsight] Error stack:', error.stack);
}
throw error;
}
}
// Export client getter for tools
export function getClient() {
return client;
}

View file

@ -0,0 +1,32 @@
// Type definitions for moltbot plugin SDK
// These are minimal types based on the documentation
declare module 'moltbot/plugin-sdk' {
export interface HookEvent {
type: 'command' | 'session' | 'agent' | 'gateway' | 'tool_result_persist';
action?: string;
sessionKey?: string;
timestamp?: string;
messages?: string[];
context?: {
sessionEntry?: {
messages?: Array<{
role: string;
content: string;
}>;
};
sessionId?: string;
sessionKey?: string;
sessionFile?: string;
commandSource?: string;
senderId?: string;
workspaceDir?: string;
bootstrapFiles?: string[];
cfg?: any;
};
}
export type HookHandler = (event: HookEvent) => Promise<void>;
export function registerPluginHooksFromDir(api: any, dir: string): void;
}

View file

@ -0,0 +1,84 @@
// Moltbot plugin API types (minimal subset needed for this plugin)
export interface MoltbotPluginAPI {
config: MoltbotConfig;
registerService(config: ServiceConfig): void;
on(event: string, handler: (context: any) => void | Promise<void | { prependContext?: string }>): void;
// Add more as needed
}
export interface MoltbotConfig {
agents?: {
defaults?: {
models?: {
[modelName: string]: {
alias?: string;
};
};
};
};
plugins?: {
entries?: {
[pluginId: string]: {
enabled?: boolean;
config?: PluginConfig;
};
};
};
}
export interface PluginConfig {
bankMission?: string;
embedPort?: number;
daemonIdleTimeout?: number; // Seconds before daemon shuts down (0 = never)
}
export interface ServiceConfig {
id: string;
start(): Promise<void>;
stop(): Promise<void>;
}
// Hindsight API types
export interface RetainRequest {
content: string;
document_id?: string;
metadata?: Record<string, unknown>;
}
export interface RetainResponse {
message: string;
document_id: string;
memory_unit_ids: string[];
}
export interface RecallRequest {
query: string;
max_tokens?: number;
}
export interface RecallResponse {
results: MemoryResult[];
}
export interface MemoryResult {
content: string;
score: number;
metadata?: {
document_id?: string;
created_at?: string;
source?: string;
};
}
export interface CreateBankRequest {
name: string;
background_context?: string;
}
export interface CreateBankResponse {
bank_id: string;
name: string;
created_at: string;
}

View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"moduleResolution": "node",
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "skills"]
}

View file

@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/*.test.ts'],
},
});