feat: simplify mcp installation + ui standalone (#41)

This commit is contained in:
Nicolò Boschi 2025-12-18 10:24:56 +01:00 committed by GitHub
parent 8ecb5d3a0c
commit 1c6acc3ba0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 729 additions and 5503 deletions

View file

@ -117,6 +117,47 @@ jobs:
path: hindsight-clients/typescript/*.tgz path: hindsight-clients/typescript/*.tgz
retention-days: 1 retention-days: 1
release-control-plane:
runs-on: ubuntu-latest
environment: npm
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Build TypeScript client (dependency)
run: npm run build --workspace=hindsight-clients/typescript
- name: Build
run: npm run build --workspace=hindsight-control-plane
- name: Publish to npm
working-directory: ./hindsight-control-plane
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Pack for GitHub release
working-directory: ./hindsight-control-plane
run: npm pack
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: control-plane
path: hindsight-control-plane/*.tgz
retention-days: 1
release-rust-cli: release-rust-cli:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@ -287,7 +328,7 @@ jobs:
create-github-release: create-github-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [release-python-packages, release-typescript-client, release-rust-cli, release-docker-images, release-helm-chart] needs: [release-python-packages, release-typescript-client, release-control-plane, release-rust-cli, release-docker-images, release-helm-chart]
permissions: permissions:
contents: write contents: write
@ -310,6 +351,12 @@ jobs:
name: typescript-client name: typescript-client
path: ./artifacts/typescript-client path: ./artifacts/typescript-client
- name: Download Control Plane
uses: actions/download-artifact@v4
with:
name: control-plane
path: ./artifacts/control-plane
- name: Download Rust CLI (Linux) - name: Download Rust CLI (Linux)
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@ -344,6 +391,8 @@ jobs:
cp artifacts/python-packages/hindsight-integrations/litellm/dist/* release-assets/ || true cp artifacts/python-packages/hindsight-integrations/litellm/dist/* release-assets/ || true
# TypeScript client # TypeScript client
cp artifacts/typescript-client/*.tgz release-assets/ || true cp artifacts/typescript-client/*.tgz release-assets/ || true
# Control Plane
cp artifacts/control-plane/*.tgz release-assets/ || true
# Rust CLI binaries # Rust CLI binaries
cp artifacts/rust-cli-linux/hindsight-linux-amd64 release-assets/ || true cp artifacts/rust-cli-linux/hindsight-linux-amd64 release-assets/ || true
cp artifacts/rust-cli-darwin-amd64/hindsight-darwin-amd64 release-assets/ || true cp artifacts/rust-cli-darwin-amd64/hindsight-darwin-amd64 release-assets/ || true

View file

@ -80,6 +80,33 @@ jobs:
- name: Build TypeScript client - name: Build TypeScript client
run: npm run build --workspace=hindsight-clients/typescript run: npm run build --workspace=hindsight-clients/typescript
build-control-plane:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Build TypeScript client (dependency)
run: npm run build --workspace=hindsight-clients/typescript
- name: Build control plane
run: npm run build --workspace=hindsight-control-plane
- name: Verify standalone build
run: |
test -f hindsight-control-plane/standalone/server.js || exit 1
node hindsight-control-plane/bin/cli.js --help
build-docs: build-docs:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View file

@ -72,30 +72,39 @@ FROM node:20-slim AS cp-builder
ARG INCLUDE_CP ARG INCLUDE_CP
RUN if [ "$INCLUDE_CP" != "true" ]; then echo "Skipping CP build" && exit 0; fi RUN if [ "$INCLUDE_CP" != "true" ]; then echo "Skipping CP build" && exit 0; fi
WORKDIR /app # Create directory structure matching the monorepo layout
# This is required because build:standalone script expects .next/standalone/memory-poc/hindsight-control-plane
# Copy built SDK WORKDIR /app/memory-poc/hindsight-control-plane
COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
# Install Control Plane dependencies # Install Control Plane dependencies
# Only copy package.json (not package-lock.json) to ensure npm installs # Only copy package.json (not package-lock.json) to ensure npm installs
# correct platform-specific native bindings for lightningcss/tailwindcss # correct platform-specific native bindings for lightningcss/tailwindcss
COPY hindsight-control-plane/package.json ./ COPY hindsight-control-plane/package.json ./
# Remove the file: dependency on SDK (we'll copy it directly later)
RUN sed -i '/"@vectorize-io\/hindsight-client":/d' package.json
RUN npm install RUN npm install
# Copy Control Plane source (excluding node_modules via .dockerignore) # Copy Control Plane source (excluding node_modules via .dockerignore)
COPY hindsight-control-plane/ ./ COPY hindsight-control-plane/ ./
# Remove package-lock.json to avoid conflicts with installed native bindings # Remove package-lock.json to avoid conflicts with installed native bindings
RUN rm -f package-lock.json # Also remove the file: dependency from package.json (restored by COPY above)
RUN rm -f package-lock.json && sed -i '/"@vectorize-io\/hindsight-client":/d' package.json
# Link SDK (temporary for build) # Copy built SDK directly into node_modules (more reliable than npm link in Docker)
RUN cd /app/sdk && npm link && cd /app && npm link @vectorize-io/hindsight-client COPY --from=sdk-builder /app/hindsight-clients/typescript ./node_modules/@vectorize-io/hindsight-client
# Build Control Plane # Build Control Plane - run next build first, then custom standalone copy
RUN npm run build # (The build:standalone script expects a specific path structure that differs in Docker)
RUN npm exec -- next build
# Create public directory if it doesn't exist # Create standalone directory structure manually
RUN mkdir -p public # Next.js standalone output structure varies, so we find server.js and work from there
RUN mkdir -p standalone/.next && \
STANDALONE_ROOT=$(dirname $(find .next/standalone -name "server.js" | head -1)) && \
cp -r "$STANDALONE_ROOT"/* standalone/ && \
cp -r .next/static standalone/.next/static && \
mkdir -p standalone/public && \
cp -r public/* standalone/public/ 2>/dev/null || true
# ============================================================================= # =============================================================================
# Stage: Final Image - API Only # Stage: Final Image - API Only
@ -172,9 +181,9 @@ COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
# Copy Control Plane standalone build # Copy Control Plane standalone build
WORKDIR /app/control-plane WORKDIR /app/control-plane
COPY --from=cp-builder /app/.next/standalone ./ COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/standalone ./
COPY --from=cp-builder /app/.next/static ./.next/static COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/.next/static ./.next/static
COPY --from=cp-builder /app/public ./public COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/public ./public
WORKDIR /app WORKDIR /app
@ -226,9 +235,9 @@ COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
# Copy Control Plane standalone build # Copy Control Plane standalone build
WORKDIR /app/control-plane WORKDIR /app/control-plane
COPY --from=cp-builder /app/.next/standalone ./ COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/standalone ./
COPY --from=cp-builder /app/.next/static ./.next/static COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/.next/static ./.next/static
COPY --from=cp-builder /app/public ./public COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/public ./public
WORKDIR /app WORKDIR /app

View file

@ -31,6 +31,7 @@ ENV_LOG_LEVEL = "HINDSIGHT_API_LOG_LEVEL"
ENV_MCP_ENABLED = "HINDSIGHT_API_MCP_ENABLED" ENV_MCP_ENABLED = "HINDSIGHT_API_MCP_ENABLED"
ENV_GRAPH_RETRIEVER = "HINDSIGHT_API_GRAPH_RETRIEVER" ENV_GRAPH_RETRIEVER = "HINDSIGHT_API_GRAPH_RETRIEVER"
ENV_MCP_LOCAL_BANK_ID = "HINDSIGHT_API_MCP_LOCAL_BANK_ID" ENV_MCP_LOCAL_BANK_ID = "HINDSIGHT_API_MCP_LOCAL_BANK_ID"
ENV_MCP_INSTRUCTIONS = "HINDSIGHT_API_MCP_INSTRUCTIONS"
# Default values # Default values
DEFAULT_DATABASE_URL = "pg0" DEFAULT_DATABASE_URL = "pg0"
@ -50,6 +51,26 @@ DEFAULT_MCP_ENABLED = True
DEFAULT_GRAPH_RETRIEVER = "bfs" # Options: "bfs", "mpfp" DEFAULT_GRAPH_RETRIEVER = "bfs" # Options: "bfs", "mpfp"
DEFAULT_MCP_LOCAL_BANK_ID = "mcp" DEFAULT_MCP_LOCAL_BANK_ID = "mcp"
# Default MCP tool descriptions (can be customized via env vars)
DEFAULT_MCP_RETAIN_DESCRIPTION = """Store important information to long-term memory.
Use this tool PROACTIVELY whenever the user shares:
- Personal facts, preferences, or interests
- Important events or milestones
- User history, experiences, or background
- Decisions, opinions, or stated preferences
- Goals, plans, or future intentions
- Relationships or people mentioned
- Work context, projects, or responsibilities"""
DEFAULT_MCP_RECALL_DESCRIPTION = """Search memories to provide personalized, context-aware responses.
Use this tool PROACTIVELY to:
- Check user's preferences before making suggestions
- Recall user's history to provide continuity
- Remember user's goals and context
- Personalize responses based on past interactions"""
# Required embedding dimension for database schema # Required embedding dimension for database schema
EMBEDDING_DIMENSION = 384 EMBEDDING_DIMENSION = 384
@ -142,7 +163,9 @@ class HindsightConfig:
def configure_logging(self) -> None: def configure_logging(self) -> None:
"""Configure Python logging based on the log level.""" """Configure Python logging based on the log level."""
logging.basicConfig( logging.basicConfig(
level=self.get_python_log_level(), format="%(asctime)s - %(levelname)s - %(name)s - %(message)s" level=self.get_python_log_level(),
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
force=True, # Override any existing configuration
) )
def log_config(self) -> None: def log_config(self) -> None:

View file

@ -172,7 +172,7 @@ class LLMProvider:
# Check if model supports reasoning parameter (o1, o3, gpt-5 families) # Check if model supports reasoning parameter (o1, o3, gpt-5 families)
model_lower = self.model.lower() model_lower = self.model.lower()
is_reasoning_model = any(x in model_lower for x in ["gpt-5", "o1", "o3"]) is_reasoning_model = any(x in model_lower for x in ["gpt-5", "o1", "o3", "deepseek"])
# For GPT-4 and GPT-4.1 models, cap max_completion_tokens to 32000 # For GPT-4 and GPT-4.1 models, cap max_completion_tokens to 32000
# For GPT-4o models, cap to 16384 # For GPT-4o models, cap to 16384
@ -194,7 +194,7 @@ class LLMProvider:
call_params["temperature"] = temperature call_params["temperature"] = temperature
# Set reasoning_effort for reasoning models (OpenAI gpt-5, o1, o3) # Set reasoning_effort for reasoning models (OpenAI gpt-5, o1, o3)
if is_reasoning_model and self.provider == "openai": if is_reasoning_model:
call_params["reasoning_effort"] = self.reasoning_effort call_params["reasoning_effort"] = self.reasoning_effort
# Provider-specific parameters # Provider-specific parameters
@ -203,7 +203,6 @@ class LLMProvider:
extra_body = {"service_tier": "auto"} extra_body = {"service_tier": "auto"}
# Only add reasoning parameters for reasoning models # Only add reasoning parameters for reasoning models
if is_reasoning_model: if is_reasoning_model:
extra_body["reasoning_effort"] = self.reasoning_effort
extra_body["include_reasoning"] = False extra_body["include_reasoning"] = False
call_params["extra_body"] = extra_body call_params["extra_body"] = extra_body

View file

@ -107,6 +107,10 @@ async def retain_batch(
) )
if not extracted_facts: if not extracted_facts:
total_time = time.time() - start_time
logger.info(
f"RETAIN_BATCH COMPLETE: 0 facts extracted from {len(contents)} contents in {total_time:.3f}s (nothing to store)"
)
return [[] for _ in contents] return [[] for _ in contents]
# Apply fact_type_override if provided # Apply fact_type_override if provided

View file

@ -127,8 +127,10 @@ def main():
port=args.port, port=args.port,
log_level=args.log_level, log_level=args.log_level,
mcp_enabled=config.mcp_enabled, mcp_enabled=config.mcp_enabled,
graph_retriever=config.graph_retriever,
) )
config.configure_logging() config.configure_logging()
config.log_config()
# Register cleanup handlers # Register cleanup handlers
atexit.register(_cleanup) atexit.register(_cleanup)

View file

@ -28,7 +28,15 @@ Environment variables:
HINDSIGHT_API_LLM_PROVIDER: Optional. LLM provider (default: "openai"). HINDSIGHT_API_LLM_PROVIDER: Optional. LLM provider (default: "openai").
HINDSIGHT_API_LLM_MODEL: Optional. LLM model (default: "gpt-4o-mini"). HINDSIGHT_API_LLM_MODEL: Optional. LLM model (default: "gpt-4o-mini").
HINDSIGHT_API_MCP_LOCAL_BANK_ID: Optional. Memory bank ID (default: "mcp"). HINDSIGHT_API_MCP_LOCAL_BANK_ID: Optional. Memory bank ID (default: "mcp").
HINDSIGHT_API_LOG_LEVEL: Optional. Log level (default: "info"). HINDSIGHT_API_LOG_LEVEL: Optional. Log level (default: "warning").
HINDSIGHT_API_MCP_INSTRUCTIONS: Optional. Additional instructions appended to both retain and recall tools.
Example custom instructions (these are ADDED to the default behavior):
To also store assistant actions:
HINDSIGHT_API_MCP_INSTRUCTIONS="Also store every action you take, including tool calls, code written, and decisions made."
To also store conversation summaries:
HINDSIGHT_API_MCP_INSTRUCTIONS="Also store summaries of important conversations and their outcomes."
""" """
import logging import logging
@ -36,14 +44,19 @@ import os
import sys import sys
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from mcp.types import Icon
from hindsight_api.config import ( from hindsight_api.config import (
DEFAULT_MCP_LOCAL_BANK_ID, DEFAULT_MCP_LOCAL_BANK_ID,
DEFAULT_MCP_RECALL_DESCRIPTION,
DEFAULT_MCP_RETAIN_DESCRIPTION,
ENV_MCP_INSTRUCTIONS,
ENV_MCP_LOCAL_BANK_ID, ENV_MCP_LOCAL_BANK_ID,
) )
# Configure logging - default to info # Configure logging - default to warning to avoid polluting stderr during MCP init
_log_level_str = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "info").lower() # MCP clients interpret stderr output as errors, so we suppress INFO logs by default
_log_level_str = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "warning").lower()
_log_level_map = { _log_level_map = {
"critical": logging.CRITICAL, "critical": logging.CRITICAL,
"error": logging.ERROR, "error": logging.ERROR,
@ -79,22 +92,21 @@ def create_local_mcp_server(bank_id: str, memory=None) -> FastMCP:
if memory is None: if memory is None:
memory = MemoryEngine(db_url="pg0://hindsight-mcp") memory = MemoryEngine(db_url="pg0://hindsight-mcp")
# Get custom instructions from environment variable (appended to both tools)
extra_instructions = os.environ.get(ENV_MCP_INSTRUCTIONS, "")
retain_description = DEFAULT_MCP_RETAIN_DESCRIPTION
recall_description = DEFAULT_MCP_RECALL_DESCRIPTION
if extra_instructions:
retain_description = f"{DEFAULT_MCP_RETAIN_DESCRIPTION}\n\nAdditional instructions: {extra_instructions}"
recall_description = f"{DEFAULT_MCP_RECALL_DESCRIPTION}\n\nAdditional instructions: {extra_instructions}"
mcp = FastMCP("hindsight") mcp = FastMCP("hindsight")
@mcp.tool() @mcp.tool(description=retain_description)
async def retain(content: str, context: str = "general") -> dict: async def retain(content: str, context: str = "general") -> dict:
""" """
Store important information to long-term memory.
Use this tool PROACTIVELY whenever the user shares:
- Personal facts, preferences, or interests
- Important events or milestones
- User history, experiences, or background
- Decisions, opinions, or stated preferences
- Goals, plans, or future intentions
- Relationships or people mentioned
- Work context, projects, or responsibilities
Args: Args:
content: The fact/memory to store (be specific and include relevant details) content: The fact/memory to store (be specific and include relevant details)
context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general' context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
@ -111,17 +123,9 @@ def create_local_mcp_server(bank_id: str, memory=None) -> FastMCP:
asyncio.create_task(_retain()) asyncio.create_task(_retain())
return {"status": "accepted", "message": "Memory storage initiated"} return {"status": "accepted", "message": "Memory storage initiated"}
@mcp.tool() @mcp.tool(description=recall_description)
async def recall(query: str, max_tokens: int = 4096, budget: str = "low") -> dict: async def recall(query: str, max_tokens: int = 4096, budget: str = "low") -> dict:
""" """
Search memories to provide personalized, context-aware responses.
Use this tool PROACTIVELY to:
- Check user's preferences before making suggestions
- Recall user's history to provide continuity
- Remember user's goals and context
- Personalize responses based on past interactions
Args: Args:
query: Natural language search query (e.g., "user's food preferences", "what projects is user working on") query: Natural language search query (e.g., "user's food preferences", "what projects is user working on")
max_tokens: Maximum tokens to return in results (default: 4096) max_tokens: Maximum tokens to return in results (default: 4096)
@ -153,10 +157,9 @@ async def _initialize_and_run(bank_id: str):
from hindsight_api import MemoryEngine from hindsight_api import MemoryEngine
# Create and initialize memory engine with pg0 embedded database # Create and initialize memory engine with pg0 embedded database
print("Initializing memory engine...", file=sys.stderr) # Note: We avoid printing to stderr during init as MCP clients show it as "errors"
memory = MemoryEngine(db_url="pg0://hindsight-mcp") memory = MemoryEngine(db_url="pg0://hindsight-mcp")
await memory.initialize() await memory.initialize()
print("Memory engine initialized.", file=sys.stderr)
# Create and run the server # Create and run the server
mcp = create_local_mcp_server(bank_id, memory=memory) mcp = create_local_mcp_server(bank_id, memory=memory)
@ -179,8 +182,8 @@ def main():
# Get bank ID from environment, default to "mcp" # Get bank ID from environment, default to "mcp"
bank_id = os.environ.get(ENV_MCP_LOCAL_BANK_ID, DEFAULT_MCP_LOCAL_BANK_ID) bank_id = os.environ.get(ENV_MCP_LOCAL_BANK_ID, DEFAULT_MCP_LOCAL_BANK_ID)
# Print startup message to stderr (stdout is reserved for MCP protocol) # Note: We don't print to stderr as MCP clients display it as "error output"
print(f"Hindsight MCP server starting (bank_id={bank_id})...", file=sys.stderr) # Use HINDSIGHT_API_LOG_LEVEL=debug for verbose startup logging
# Run the async initialization and server # Run the async initialization and server
asyncio.run(_initialize_and_run(bank_id)) asyncio.run(_initialize_and_run(bank_id))

View file

@ -91,6 +91,9 @@ enum Commands {
#[command(alias = "tui")] #[command(alias = "tui")]
Explore, Explore,
/// Launch the web-based control plane UI
Ui,
/// Configure the CLI (API URL, etc.) /// Configure the CLI (API URL, etc.)
#[command(after_help = "Configuration priority:\n 1. Environment variable (HINDSIGHT_API_URL) - highest priority\n 2. Config file (~/.hindsight/config)\n 3. Default (http://localhost:8888)")] #[command(after_help = "Configuration priority:\n 1. Environment variable (HINDSIGHT_API_URL) - highest priority\n 2. Config file (~/.hindsight/config)\n 3. Default (http://localhost:8888)")]
Configure { Configure {
@ -373,6 +376,11 @@ fn run() -> Result<()> {
return handle_configure(api_url, output_format); return handle_configure(api_url, output_format);
} }
// Handle ui command - needs config but not API client
if let Commands::Ui = cli.command {
return handle_ui(output_format);
}
// Load configuration // Load configuration
let config = Config::from_env().unwrap_or_else(|e| { let config = Config::from_env().unwrap_or_else(|e| {
ui::print_error(&format!("Configuration error: {}", e)); ui::print_error(&format!("Configuration error: {}", e));
@ -390,6 +398,7 @@ fn run() -> Result<()> {
// Execute command and handle errors // Execute command and handle errors
let result: Result<()> = match cli.command { let result: Result<()> = match cli.command {
Commands::Configure { .. } => unreachable!(), // Handled above Commands::Configure { .. } => unreachable!(), // Handled above
Commands::Ui => unreachable!(), // Handled above
Commands::Explore => commands::explore::run(&client), Commands::Explore => commands::explore::run(&client),
Commands::Bank(bank_cmd) => match bank_cmd { Commands::Bank(bank_cmd) => match bank_cmd {
BankCommands::List => commands::bank::list(&client, verbose, output_format), BankCommands::List => commands::bank::list(&client, verbose, output_format),
@ -521,3 +530,50 @@ fn handle_configure(api_url: Option<String>, output_format: OutputFormat) -> Res
Ok(()) Ok(())
} }
fn handle_ui(output_format: OutputFormat) -> Result<()> {
use std::process::Command;
// Load configuration to get the API URL
let config = Config::load().unwrap_or_else(|e| {
ui::print_error(&format!("Configuration error: {}", e));
errors::print_config_help();
std::process::exit(1);
});
let api_url = config.api_url();
if output_format == OutputFormat::Pretty {
ui::print_info("Launching Hindsight Control Plane UI...");
println!();
println!(" API URL: {}", api_url);
println!();
}
// Run npx @vectorize-io/hindsight-control-plane --api-url {api_url}
let status = Command::new("npx")
.arg("@vectorize-io/hindsight-control-plane")
.arg("--api-url")
.arg(api_url)
.status();
match status {
Ok(exit_status) => {
if !exit_status.success() {
if let Some(code) = exit_status.code() {
std::process::exit(code);
} else {
std::process::exit(1);
}
}
}
Err(e) => {
ui::print_error(&format!("Failed to launch control plane UI: {}", e));
ui::print_info("Make sure you have Node.js and npm installed.");
ui::print_info("You can also install the control plane globally: npm install -g @vectorize-io/hindsight-control-plane");
std::process::exit(1);
}
}
Ok(())
}

View file

@ -14,6 +14,7 @@
# production # production
/build /build
/standalone
# misc # misc
.DS_Store .DS_Store

View file

@ -0,0 +1,86 @@
#!/usr/bin/env node
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const args = process.argv.slice(2);
// Parse command line arguments
let port = process.env.PORT || 9999;
let hostname = process.env.HOSTNAME || '0.0.0.0';
let apiUrl = process.env.HINDSIGHT_CP_DATAPLANE_API_URL;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--port' || args[i] === '-p') {
port = args[++i];
} else if (args[i] === '--hostname' || args[i] === '-H') {
hostname = args[++i];
} else if (args[i] === '--api-url' || args[i] === '-a') {
apiUrl = args[++i];
} else if (args[i] === '--help' || args[i] === '-h') {
console.log(`
Hindsight Control Plane
Usage: hindsight-control-plane [options]
Options:
-p, --port <port> Port to listen on (default: 9999, env: PORT)
-H, --hostname <host> Hostname to bind to (default: 0.0.0.0, env: HOSTNAME)
-a, --api-url <url> Hindsight API URL (env: HINDSIGHT_CP_DATAPLANE_API_URL)
-h, --help Show this help message
Environment Variables:
PORT Port to listen on
HOSTNAME Hostname to bind to
HINDSIGHT_CP_DATAPLANE_API_URL URL of the Hindsight API server
`);
process.exit(0);
}
}
// Find the standalone server
const standaloneDir = path.join(__dirname, '..', 'standalone');
const serverPath = path.join(standaloneDir, 'server.js');
if (!fs.existsSync(serverPath)) {
console.error('Error: Standalone server not found at', serverPath);
console.error('This package may not have been built correctly.');
process.exit(1);
}
// Set up environment
const env = {
...process.env,
PORT: String(port),
HOSTNAME: hostname,
};
if (apiUrl) {
env.HINDSIGHT_CP_DATAPLANE_API_URL = apiUrl;
}
console.log(`Starting Hindsight Control Plane on http://${hostname}:${port}`);
if (apiUrl) {
console.log(`API URL: ${apiUrl}`);
}
// Run the standalone server
const server = spawn('node', [serverPath], {
cwd: standaloneDir,
env,
stdio: 'inherit',
});
server.on('error', (err) => {
console.error('Failed to start server:', err.message);
process.exit(1);
});
server.on('close', (code) => {
process.exit(code || 0);
});
// Handle signals
process.on('SIGTERM', () => server.kill('SIGTERM'));
process.on('SIGINT', () => server.kill('SIGINT'));

View file

@ -1,17 +1,26 @@
{ {
"name": "hindsight-control-plane", "name": "@vectorize-io/hindsight-control-plane",
"version": "0.1.8", "version": "0.1.8",
"private": true, "description": "Control plane for Hindsight - Semantic memory system",
"bin": {
"hindsight-control-plane": "./bin/cli.js"
},
"files": [
"bin",
"standalone",
"public"
],
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build && npm run build:standalone",
"build:standalone": "rm -rf standalone && STANDALONE_ROOT=$(dirname $(find .next/standalone -name 'server.js' | head -1)) && cp -r \"$STANDALONE_ROOT\" standalone && mkdir -p standalone/.next && cp -r .next/static standalone/.next/static && mkdir -p standalone/public && cp -r public/* standalone/public/ 2>/dev/null || true",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"prepublishOnly": "npm run build"
}, },
"keywords": [], "keywords": ["hindsight", "memory", "semantic", "ai"],
"author": "Hindsight Team", "author": "Hindsight Team",
"license": "ISC", "license": "ISC",
"description": "Control plane for Hindsight - Semantic memory system",
"dependencies": { "dependencies": {
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
@ -27,7 +36,6 @@
"@types/node": "^24.10.0", "@types/node": "^24.10.0",
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2", "@types/react-dom": "^19.2.2",
"@vectorize-io/hindsight-client": "file:../hindsight-clients/typescript",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -50,6 +58,7 @@
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"devDependencies": { "devDependencies": {
"@vectorize-io/hindsight-client": "file:../hindsight-clients/typescript",
"@eslint/eslintrc": "^3.3.3", "@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",

View file

@ -133,6 +133,38 @@ hindsight-api --workers 4 # Multiple worker processes
hindsight-api --log-level debug # Verbose logging hindsight-api --log-level debug # Verbose logging
``` ```
### Control Plane
The Control Plane (Web UI) can be run standalone using npx:
```bash
npx @vectorize-io/hindsight-control-plane --api-url http://localhost:8888
```
This connects to your running API server and provides a visual interface for managing memory banks, exploring entities, and testing queries.
#### Options
| Option | Environment Variable | Default | Description |
|--------|---------------------|---------|-------------|
| `-p, --port` | `PORT` | 9999 | Port to listen on |
| `-H, --hostname` | `HOSTNAME` | 0.0.0.0 | Hostname to bind to |
| `-a, --api-url` | `HINDSIGHT_CP_DATAPLANE_API_URL` | http://localhost:8888 | Hindsight API URL |
#### Examples
```bash
# Run on custom port
npx @vectorize-io/hindsight-control-plane --port 9999 --api-url http://localhost:8888
# Using environment variables
export HINDSIGHT_CP_DATAPLANE_API_URL=http://api.example.com
npx @vectorize-io/hindsight-control-plane
# Production deployment
PORT=80 HINDSIGHT_CP_DATAPLANE_API_URL=https://api.hindsight.io npx @vectorize-io/hindsight-control-plane
```
--- ---
## Next Steps ## Next Steps

View file

@ -25,12 +25,10 @@ Web UI for managing and exploring your memory banks:
- View ingestion history and operations - View ingestion history and operations
- Test recall queries interactively - Test recall queries interactively
```
hindsight-control-plane # Default port: 9999
```
The Control Plane connects to the API service and provides a visual interface for development and debugging. The Control Plane connects to the API service and provides a visual interface for development and debugging.
For bare metal deployments, you can run the Control Plane standalone using npx. See [Installation - Bare Metal](./installation#control-plane) for details.
## Deployment Options ## Deployment Options
| Deployment | Services | Use Case | | Deployment | Services | Use Case |
@ -39,4 +37,4 @@ The Control Plane connects to the API service and provides a visual interface fo
| **Helm / Kubernetes** | Separate pods | Production, scaling | | **Helm / Kubernetes** | Separate pods | Production, scaling |
| **Bare metal** | Run independently | Custom deployments | | **Bare metal** | Run independently | Custom deployments |
In the Docker quickstart, both services run in a single container. For production Kubernetes deployments, they run as separate pods with independent scaling. In the Docker quickstart, both services run in a single container. For production Kubernetes deployments, they run as separate pods with independent scaling. For bare metal, you can run the API via pip and the Control Plane via npx.

View file

@ -177,6 +177,25 @@ hindsight memory recall <bank_id> "query" -o yaml
| `--help` | Show help | | `--help` | Show help |
| `--version` | Show version | | `--version` | Show version |
## Control Plane UI
Launch the web-based Control Plane UI directly from the CLI:
```bash
hindsight ui
```
This runs the Control Plane locally on port 9999 using the API URL from your configuration. The UI provides:
- **Memory bank management** — Browse and manage all your banks
- **Entity explorer** — Visualize the knowledge graph
- **Query testing** — Interactive recall and reflect testing
- **Operation history** — View ingestion and processing logs
:::tip
The UI command requires Node.js to be installed. It automatically downloads and runs the `@vectorize-io/hindsight-control-plane` package via npx.
:::
## Interactive Explorer ## Interactive Explorer
Launch the TUI explorer for visual navigation of your memory banks: Launch the TUI explorer for visual navigation of your memory banks:

View file

@ -7,28 +7,35 @@ sidebar_position: 2
Hindsight provides a fully local MCP server that runs entirely on your machine with an embedded PostgreSQL database. No external server or database setup required. Hindsight provides a fully local MCP server that runs entirely on your machine with an embedded PostgreSQL database. No external server or database setup required.
This is ideal for: This is ideal for:
- **Personal use with Claude Code** — Give Claude long-term memory across conversations - **Personal use with Claude Desktop** — Give Claude long-term memory across conversations
- **Development and testing** — Quick setup without infrastructure - **Development and testing** — Quick setup without infrastructure
- **Privacy-focused setups** — All data stays on your machine - **Privacy-focused setups** — All data stays on your machine
## Quick Start ## Quick Install
### With uvx (recommended)
```bash ```bash
uvx --from hindsight-api hindsight-local-mcp curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- \
--app claude-desktop \
--set HINDSIGHT_API_LLM_API_KEY=sk-...
``` ```
### With pip This script will:
1. Install [uv](https://docs.astral.sh/uv/) if not already installed
2. Configure Claude Desktop to use the Hindsight MCP server
3. Set the provided environment variables in the MCP configuration
```bash :::info Other MCP Applications
pip install hindsight-api The quick install script currently supports Claude Desktop only. For other MCP-compatible applications (Cursor, Cline, etc.), follow the [Manual Configuration](#manual-configuration) steps below.
hindsight-local-mcp :::
```
## Claude Code Configuration ## Manual Configuration
Add to your Claude Code MCP settings (`~/.claude/claude_desktop_config.json`): Add the following to your MCP client's configuration. For Claude Desktop:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
For other MCP clients, refer to their documentation for the configuration file location.
```json ```json
{ {
@ -37,7 +44,7 @@ Add to your Claude Code MCP settings (`~/.claude/claude_desktop_config.json`):
"command": "uvx", "command": "uvx",
"args": ["--from", "hindsight-api", "hindsight-local-mcp"], "args": ["--from", "hindsight-api", "hindsight-local-mcp"],
"env": { "env": {
"HINDSIGHT_API_LLM_API_KEY": "your-openai-key" "HINDSIGHT_API_LLM_API_KEY": "sk-..."
} }
} }
} }
@ -55,7 +62,7 @@ By default, memories are stored in a bank called `mcp`. To use a different bank:
"command": "uvx", "command": "uvx",
"args": ["--from", "hindsight-api", "hindsight-local-mcp"], "args": ["--from", "hindsight-api", "hindsight-local-mcp"],
"env": { "env": {
"HINDSIGHT_API_LLM_API_KEY": "your-openai-key", "HINDSIGHT_API_LLM_API_KEY": "sk-...",
"HINDSIGHT_API_MCP_LOCAL_BANK_ID": "my-personal-memory" "HINDSIGHT_API_MCP_LOCAL_BANK_ID": "my-personal-memory"
} }
} }
@ -72,6 +79,20 @@ All standard [Hindsight configuration variables](/developer/configuration) are s
| Variable | Required | Default | Description | | Variable | Required | Default | Description |
|----------|----------|---------|-------------| |----------|----------|---------|-------------|
| `HINDSIGHT_API_MCP_LOCAL_BANK_ID` | No | `mcp` | Memory bank ID to use | | `HINDSIGHT_API_MCP_LOCAL_BANK_ID` | No | `mcp` | Memory bank ID to use |
| `HINDSIGHT_API_MCP_INSTRUCTIONS` | No | - | Additional instructions appended to both `retain` and `recall` tools |
### Customizing Tool Behavior
You can customize what gets stored by adding instructions to the tools. Re-run the install script with the additional `--set` flag:
```bash
curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- \
--app claude-desktop \
--set HINDSIGHT_API_LLM_API_KEY=sk-... \
--set HINDSIGHT_API_MCP_INSTRUCTIONS="Also store every action you take, code you write, and files you modify."
```
These instructions are appended to the default tool descriptions, guiding Claude on when and how to use the memory tools.
## Available Tools ## Available Tools

301
hindsight-docs/static/get-mcp Executable file
View file

@ -0,0 +1,301 @@
#!/bin/bash
#
# Install Hindsight MCP server for Claude Desktop
#
# Usage:
# curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- --app claude-desktop --set HINDSIGHT_API_LLM_API_KEY=YOUR_KEY
#
# Options:
# --app Required. Target application (currently only: claude-desktop)
# --set ENV=VALUE Set environment variable (can be repeated)
#
# Examples:
# # With OpenAI
# curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- \
# --app claude-desktop \
# --set HINDSIGHT_API_LLM_API_KEY=sk-...
#
# # With Ollama (local LLM)
# curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- \
# --app claude-desktop \
# --set HINDSIGHT_API_LLM_PROVIDER=ollama \
# --set HINDSIGHT_API_LLM_MODEL=llama3.2
#
# # With custom memory instructions
# curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- \
# --app claude-desktop \
# --set HINDSIGHT_API_LLM_API_KEY=sk-... \
# --set HINDSIGHT_API_MCP_INSTRUCTIONS="Also store every action you take and code you write."
#
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_info() {
echo -e "${BLUE}${NC} $1"
}
print_success() {
echo -e "${GREEN}✓${NC} $1"
}
print_error() {
echo -e "${RED}✗${NC} $1"
exit 1
}
print_warning() {
echo -e "${YELLOW}⚠${NC} $1"
}
print_banner() {
echo ""
# ANSI logo
echo -e " \033[38;2;9;127;184m▄\033[0m\033[48;2;8;130;178m\033[38;2;5;133;186m▄\033[0m \033[48;2;10;143;160m\033[38;2;10;143;165m▄\033[0m\033[38;2;7;140;156m▄\033[0m "
echo -e " \033[38;2;8;125;192m▄\033[0m \033[38;2;3;132;191m▀\033[0m\033[38;2;2;133;192m▄\033[0m \033[38;2;3;132;180m▄\033[0m\033[38;2;1;137;184m▄\033[0m\033[38;2;3;133;174m▄\033[0m \033[38;2;3;142;176m▄\033[0m\033[38;2;4;142;169m▀\033[0m \033[38;2;10;144;164m▄\033[0m "
echo -e "\033[38;2;6;121;195m▀\033[0m\033[38;2;5;128;203m▀\033[0m\033[48;2;5;124;195m\033[38;2;3;125;200m▄\033[0m\033[38;2;2;126;196m▄\033[0m\033[48;2;3;128;188m\033[38;2;1;131;196m▄\033[0m\033[48;2;0;152;219m\033[38;2;2;131;191m▄\033[0m\033[38;2;1;141;196m▀\033[0m\033[38;2;1;135;183m▀\033[0m\033[38;2;1;148;198m▀\033[0m\033[48;2;1;156;202m\033[38;2;2;135;180m▄\033[0m\033[48;2;4;134;169m\033[38;2;1;137;177m▄\033[0m\033[38;2;3;138;173m▄\033[0m\033[48;2;6;137;165m\033[38;2;2;140;170m▄\033[0m\033[38;2;7;144;169m▀\033[0m\033[38;2;7;139;158m▀\033[0m"
echo -e " \033[48;2;2;128;202m\033[38;2;2;124;201m▄\033[0m\033[48;2;1;130;201m\033[38;2;0;135;212m▄\033[0m\033[38;2;2;128;196m▄\033[0m \033[48;2;2;142;204m\033[38;2;7;138;199m▄\033[0m \033[38;2;1;135;186m▄\033[0m\033[48;2;1;142;186m\033[38;2;2;144;194m▄\033[0m\033[48;2;3;138;176m\033[38;2;2;134;176m▄\033[0m "
echo -e " \033[48;2;8;118;200m\033[38;2;8;121;209m▄\033[0m\033[38;2;3;121;203m▀\033[0m \033[38;2;3;122;192m▀\033[0m\033[38;2;1;138;216m▀\033[0m\033[48;2;0;138;210m\033[38;2;3;128;198m▄\033[0m\033[48;2;0;126;188m\033[38;2;2;131;198m▄\033[0m\033[48;2;0;142;205m\033[38;2;3;132;193m▄\033[0m\033[38;2;1;140;196m▀\033[0m \033[38;2;4;134;175m▀\033[0m\033[48;2;13;135;167m\033[38;2;8;136;174m▄\033[0m "
echo ""
echo -e " ${BLUE}HINDSIGHT MCP INSTALLER${NC}"
echo ""
}
# Parse arguments
APP=""
declare -a ENV_VARS=()
while [[ $# -gt 0 ]]; do
case $1 in
--app)
APP="$2"
shift 2
;;
--set)
ENV_VARS+=("$2")
shift 2
;;
-h|--help)
echo "Usage: $0 --app <app> --set ENV=VALUE [--set ENV2=VALUE2 ...]"
echo ""
echo "Options:"
echo " --app Required. Target application (currently only: claude-desktop)"
echo " --set ENV=VALUE Set environment variable (can be repeated)"
echo ""
echo "Examples:"
echo " # With OpenAI"
echo " $0 --app claude-desktop --set HINDSIGHT_API_LLM_API_KEY=sk-..."
echo ""
echo " # With Ollama (local LLM, no API key needed)"
echo " $0 --app claude-desktop --set HINDSIGHT_API_LLM_PROVIDER=ollama --set HINDSIGHT_API_LLM_MODEL=llama3.2"
exit 0
;;
*)
print_error "Unknown option: $1. Use --help for usage."
;;
esac
done
# Validate required arguments
if [ -z "$APP" ]; then
print_error "Missing required argument: --app. Use --help for usage."
fi
if [ "$APP" != "claude-desktop" ]; then
print_error "Unsupported app: $APP. Currently only 'claude-desktop' is supported."
fi
# Detect OS
detect_os() {
case "$(uname -s)" in
Darwin*) echo "macos" ;;
Linux*) echo "linux" ;;
MINGW*|MSYS*|CYGWIN*) echo "windows" ;;
*) echo "unknown" ;;
esac
}
OS=$(detect_os)
# Get Claude Desktop config path based on OS
get_claude_config_path() {
case "$OS" in
macos)
echo "$HOME/Library/Application Support/Claude/claude_desktop_config.json"
;;
linux)
echo "$HOME/.config/Claude/claude_desktop_config.json"
;;
windows)
echo "$APPDATA/Claude/claude_desktop_config.json"
;;
*)
print_error "Unsupported operating system: $OS"
;;
esac
}
# Check if uvx is installed and return its path
find_uvx() {
# Check if in PATH
if command -v uvx &> /dev/null; then
command -v uvx
return 0
fi
# Check common installation paths
local paths=(
"$HOME/.local/bin/uvx"
"$HOME/.cargo/bin/uvx"
"/usr/local/bin/uvx"
)
for path in "${paths[@]}"; do
if [ -f "$path" ]; then
echo "$path"
return 0
fi
done
return 1
}
# Install uv (which includes uvx)
install_uv() {
print_info "Installing uv..."
if [ "$OS" = "windows" ]; then
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
else
curl -LsSf https://astral.sh/uv/install.sh | sh
fi
# Source the env to get uvx in path
if [ -f "$HOME/.local/bin/env" ]; then
source "$HOME/.local/bin/env"
fi
# Find uvx again
if ! UVX_PATH=$(find_uvx); then
print_error "uv installed but uvx not found. Please check your installation."
fi
print_success "uv installed successfully"
}
# Update Claude Desktop config
update_claude_config() {
local config_path="$1"
local uvx_path="$2"
shift 2
local env_vars=("$@")
# Create config directory if it doesn't exist
mkdir -p "$(dirname "$config_path")"
# Check if jq is available (needed for both new and existing configs)
if ! command -v jq &> /dev/null; then
print_warning "jq not found. Installing..."
if [ "$OS" = "macos" ]; then
if command -v brew &> /dev/null; then
brew install jq
else
print_error "Please install jq: brew install jq"
fi
elif [ "$OS" = "linux" ]; then
if command -v apt-get &> /dev/null; then
sudo apt-get install -y jq
elif command -v yum &> /dev/null; then
sudo yum install -y jq
else
print_error "Please install jq manually"
fi
fi
fi
# Build the env object from env_vars array
local env_json="{}"
for env_var in "${env_vars[@]}"; do
local key="${env_var%%=*}"
local value="${env_var#*=}"
env_json=$(echo "$env_json" | jq --arg k "$key" --arg v "$value" '. + {($k): $v}')
done
# Build the hindsight server config
local hindsight_config
hindsight_config=$(jq -n \
--arg uvx "$uvx_path" \
--argjson env "$env_json" \
'{
"command": $uvx,
"args": ["--from", "hindsight-api", "hindsight-local-mcp"],
"env": $env
}')
# Check if config file exists and has content
if [ -f "$config_path" ] && [ -s "$config_path" ]; then
print_info "Updating existing Claude Desktop config..."
# Backup existing config
cp "$config_path" "${config_path}.backup"
print_info "Backed up existing config to ${config_path}.backup"
# Add or update hindsight server in existing config
local new_config
new_config=$(jq --argjson hs "$hindsight_config" '.mcpServers.hindsight = $hs' "$config_path")
echo "$new_config" > "$config_path"
else
print_info "Creating new Claude Desktop config..."
# Create new config with hindsight server
local new_config
new_config=$(jq -n --argjson hs "$hindsight_config" '{"mcpServers": {"hindsight": $hs}}')
echo "$new_config" > "$config_path"
fi
print_success "Claude Desktop config updated: $config_path"
}
# Main installation flow
main() {
print_banner
print_info "App: $APP"
if [ ${#ENV_VARS[@]} -gt 0 ]; then
print_info "Environment variables: ${#ENV_VARS[@]} configured"
fi
echo ""
# Step 1: Check/Install uvx
print_info "Checking for uvx..."
if UVX_PATH=$(find_uvx); then
print_success "uvx found at: $UVX_PATH"
else
print_warning "uvx not found. Installing uv..."
install_uv
UVX_PATH=$(find_uvx)
fi
# Step 2: Update Claude Desktop config
CONFIG_PATH=$(get_claude_config_path)
print_info "Configuring Claude Desktop..."
update_claude_config "$CONFIG_PATH" "$UVX_PATH" "${ENV_VARS[@]}"
# Done!
echo ""
print_success "Installation complete!"
echo ""
print_info "Next steps:"
echo " 1. Restart Claude Desktop"
echo " 2. Look for the 'hindsight' tools (retain, recall) in Claude"
echo ""
}
main "$@"

5437
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
set -e set -e
ROOT_DIR="$(git rev-parse --show-toplevel)" ROOT_DIR="$(git rev-parse --show-toplevel)"
cd "$ROOT_DIR/hindsight-control-plane" || exit 1 cd "$ROOT_DIR" || exit 1
# Check if .env exists in workspace root # Check if .env exists in workspace root
if [ ! -f "$ROOT_DIR/.env" ]; then if [ ! -f "$ROOT_DIR/.env" ]; then
@ -30,4 +30,4 @@ fi
export HOSTNAME="${HINDSIGHT_CP_HOSTNAME:-0.0.0.0}" export HOSTNAME="${HINDSIGHT_CP_HOSTNAME:-0.0.0.0}"
# Run dev server # Run dev server
npm run dev -w hindsight-control-plane npm run dev -w @vectorize-io/hindsight-control-plane