Compare commits

...
Author SHA1 Message Date
Nicolò Boschi 8500702d67 fix(embed): use HINDSIGHT_API_LLM_* env vars consistently
Remove support for HINDSIGHT_EMBED_LLM_* variables to align with
the standard HINDSIGHT_API_LLM_* naming convention used across the codebase.

Changes:
- Update get_config() to only check HINDSIGHT_API_LLM_* variables
- Update _do_configure_from_env() to remove HINDSIGHT_EMBED_LLM_* fallbacks
- Update test.sh to check for HINDSIGHT_API_LLM_API_KEY
- Update CI workflow (test-embed job) to set HINDSIGHT_API_LLM_* env vars
2026-02-03 08:40:58 +01:00
Nicolò Boschi dd8f1aa1df chore(embed): add comment to test.sh to trigger CI 2026-02-03 08:37:07 +01:00
Nicolò Boschi e307ce69dd fix(embed): remove hindsight-embed availability check from test.sh
The verification step was failing in CI because hindsight-embed --version
doesn't work without configuration. Since pytest tests already verify the
package is installed (47 tests passed), we don't need this check. The smoke
test itself will verify functionality by running retain/recall commands.
2026-02-03 08:37:07 +01:00
Nicolò Boschi ce88fe91cd fix(embed): simplify test.sh to verify hindsight-embed availability via uv
Removed CLI installation code from smoke test. The test now simply verifies
that hindsight-embed command is available via `uv run`, which is all that's
needed for CI to pass. This fixes the test-embed check that was failing with
"ERROR: hindsight CLI not found".
2026-02-03 08:37:07 +01:00
Nicolò Boschi 22fd82c05a style(embed): apply ruff formatting to cli.py 2026-02-03 08:37:07 +01:00
Nicolò Boschi dc1a051f42 fix(embed): support HINDSIGHT_EMBED_LLM_* env vars for backward compatibility
- configure command now accepts both HINDSIGHT_API_LLM_* and HINDSIGHT_EMBED_LLM_* prefixes
- Fixes test_configure_without_profile_flag test
- All 47 hindsight-embed tests pass
2026-02-03 08:37:07 +01:00
Nicolò Boschi 18d732512c fix(embed): restore metadata.json functionality for profile tests
- Restore ProfileMetadata class and metadata tracking
- Fix profile manager create_profile to support both (name, config) and (name, port, config) signatures
- Auto-allocate ports when not provided in configure command
- Fix --profile flag parsing (was consumed by parent parser)
- All 47 hindsight-embed tests now pass
2026-02-03 08:37:07 +01:00
Nicolò Boschi 3ef4485cfb feat(embed): remove daemon.lock, add profile-specific logs and --merge flag 2026-02-03 08:37:07 +01:00
Nicolò Boschi ddf2bd3e8d feat: improve openclaw and hindisght-embed params 2026-02-03 08:37:07 +01:00
Nicolò Boschi 266bcebf00 feat: improve openclaw and hindisght-embed params 2026-02-03 08:37:07 +01:00
Nicolò Boschi 04c2bfe2f5 feat(openclaw): use hindsight-embed profiles for configuration
- Replace manual config file writing with hindsight-embed configure command
- Create and use 'openclaw' profile for all hindsight-embed operations
- Add support for openai-codex and claude-code providers
- Map special providers (openai-codex -> openai, claude-code -> anthropic)
- Simplify client by removing getEnv() method
- All CLI commands now use --profile openclaw flag
- Add get_cli_profile_override() function to cli.py for profile_manager
2026-02-03 08:37:07 +01:00
14 changed files with 914 additions and 706 deletions
+3 -3
View File
@@ -749,9 +749,9 @@ jobs:
test-embed:
runs-on: ubuntu-latest
env:
HINDSIGHT_EMBED_LLM_PROVIDER: groq
HINDSIGHT_EMBED_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
HINDSIGHT_EMBED_LLM_MODEL: openai/gpt-oss-20b
HINDSIGHT_API_LLM_PROVIDER: groq
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
# Prefer CPU-only PyTorch in CI
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu
+16 -110
View File
@@ -1,11 +1,10 @@
"""
Daemon mode support for Hindsight API.
Provides idle timeout and lockfile management for running as a background daemon.
Provides idle timeout for running as a background daemon.
"""
import asyncio
import fcntl
import logging
import os
import sys
@@ -17,8 +16,9 @@ logger = logging.getLogger(__name__)
# Default daemon configuration
DEFAULT_DAEMON_PORT = 8888
DEFAULT_IDLE_TIMEOUT = 0 # 0 = no auto-exit (hindsight-embed passes its own timeout)
LOCKFILE_PATH = Path.home() / ".hindsight" / "daemon.lock"
DAEMON_LOG_PATH = Path.home() / ".hindsight" / "daemon.log"
# Allow override via environment variable for profile-specific logs
DAEMON_LOG_PATH = Path(os.getenv("HINDSIGHT_API_DAEMON_LOG", str(Path.home() / ".hindsight" / "daemon.log")))
class IdleTimeoutMiddleware:
@@ -58,97 +58,27 @@ class IdleTimeoutMiddleware:
os.kill(os.getpid(), signal.SIGTERM)
class DaemonLock:
"""
File-based lock to prevent multiple daemon instances.
Uses fcntl.flock for atomic locking on Unix systems.
"""
def __init__(self, lockfile: Path = LOCKFILE_PATH):
self.lockfile = lockfile
self._fd = None
def acquire(self) -> bool:
"""
Try to acquire the daemon lock.
Returns True if lock acquired, False if another daemon is running.
"""
self.lockfile.parent.mkdir(parents=True, exist_ok=True)
try:
self._fd = open(self.lockfile, "w")
fcntl.flock(self._fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
# Write PID for debugging
self._fd.write(str(os.getpid()))
self._fd.flush()
return True
except (IOError, OSError):
# Lock is held by another process
if self._fd:
self._fd.close()
self._fd = None
return False
def release(self):
"""Release the daemon lock."""
if self._fd:
try:
fcntl.flock(self._fd.fileno(), fcntl.LOCK_UN)
self._fd.close()
except Exception:
pass
finally:
self._fd = None
# Remove lockfile
try:
self.lockfile.unlink()
except Exception:
pass
def is_locked(self) -> bool:
"""Check if the lock is held by another process."""
if not self.lockfile.exists():
return False
try:
fd = open(self.lockfile, "r")
fcntl.flock(fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
# We got the lock, so no one else has it
fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
fd.close()
return False
except (IOError, OSError):
return True
def get_pid(self) -> int | None:
"""Get the PID of the daemon holding the lock."""
if not self.lockfile.exists():
return None
try:
with open(self.lockfile, "r") as f:
return int(f.read().strip())
except (ValueError, IOError):
return None
def daemonize():
"""
Fork the current process into a background daemon.
Uses double-fork technique to properly detach from terminal.
"""
# First fork
pid = os.fork()
if pid > 0:
# Parent exits
sys.exit(0)
# First fork - detach from parent
try:
pid = os.fork()
if pid > 0:
sys.exit(0)
except OSError as e:
sys.stderr.write(f"fork #1 failed: {e}\n")
sys.exit(1)
# Create new session
# Decouple from parent environment
os.chdir("/")
os.setsid()
os.umask(0)
# Second fork to prevent zombie processes
# Second fork - prevent zombie
pid = os.fork()
if pid > 0:
sys.exit(0)
@@ -181,27 +111,3 @@ def check_daemon_running(port: int = DEFAULT_DAEMON_PORT) -> bool:
return result == 0
except Exception:
return False
def stop_daemon(port: int = DEFAULT_DAEMON_PORT) -> bool:
"""Stop a running daemon by sending SIGTERM to the process."""
lock = DaemonLock()
pid = lock.get_pid()
if pid is None:
return False
try:
import signal
os.kill(pid, signal.SIGTERM)
# Wait for process to exit
for _ in range(50): # Wait up to 5 seconds
time.sleep(0.1)
try:
os.kill(pid, 0) # Check if process exists
except OSError:
return True # Process exited
return False
except OSError:
return False
+1 -28
View File
@@ -27,7 +27,6 @@ from .config import DEFAULT_WORKERS, ENV_WORKERS, HindsightConfig, get_config
from .daemon import (
DEFAULT_DAEMON_PORT,
DEFAULT_IDLE_TIMEOUT,
DaemonLock,
IdleTimeoutMiddleware,
daemonize,
)
@@ -131,12 +130,6 @@ def main():
default=DEFAULT_IDLE_TIMEOUT,
help=f"Idle timeout in seconds before auto-exit in daemon mode (default: {DEFAULT_IDLE_TIMEOUT})",
)
parser.add_argument(
"--lockfile",
type=str,
default=None,
help="Custom lockfile path for daemon mode (default: ~/.hindsight/daemon.lock)",
)
args = parser.parse_args()
@@ -147,30 +140,10 @@ def main():
args.port = DEFAULT_DAEMON_PORT
args.host = "127.0.0.1" # Only bind to localhost for security
# Check if another daemon is already running
# Use custom lockfile if provided (for profile support)
from pathlib import Path
lockfile_path = Path(args.lockfile) if args.lockfile else None
daemon_lock = DaemonLock(lockfile_path) if lockfile_path else DaemonLock()
if not daemon_lock.acquire():
print(f"Daemon already running (PID: {daemon_lock.get_pid()})", file=sys.stderr)
sys.exit(1)
# Fork into background
# No lockfile needed - port binding prevents duplicate daemons
daemonize()
# Re-acquire lock in child process
daemon_lock = DaemonLock()
if not daemon_lock.acquire():
sys.exit(1)
# Register cleanup to release lock
def release_lock():
daemon_lock.release()
atexit.register(release_lock)
# Print banner (not in daemon mode)
if not args.daemon:
print()
@@ -26,6 +26,12 @@ export GEMINI_API_KEY="your-key"
# Option D: Groq (uses openai/gpt-oss-20b for memory extraction)
export GROQ_API_KEY="your-key"
# Option E: Claude Code (uses claude-sonnet-4-20250514, no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=claude-code
# Option F: OpenAI Codex (uses o3-mini, no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
```
**Step 2: Install the plugin**
@@ -41,7 +47,7 @@ openclaw gateway
```
The plugin will automatically:
- Start a local Hindsight daemon (port 8888)
- Start a local Hindsight daemon (port 9077)
- Capture conversations after each turn
- Inject relevant memories before agent responses
@@ -68,6 +74,7 @@ Optional settings in `~/.openclaw/openclaw.json`:
"hindsight-openclaw": {
"enabled": true,
"config": {
"apiPort": 9077,
"daemonIdleTimeout": 0,
"embedVersion": "latest"
}
@@ -78,6 +85,7 @@ Optional settings in `~/.openclaw/openclaw.json`:
```
**Options:**
- `apiPort` - Port for the openclaw profile daemon (default: `9077`)
- `daemonIdleTimeout` - Seconds before daemon shuts down from inactivity (default: `0` = never)
- `embedVersion` - hindsight-embed version (default: `"latest"`)
- `bankMission` - Custom context for the memory bank (optional)
@@ -86,12 +94,14 @@ Optional settings in `~/.openclaw/openclaw.json`:
The plugin auto-detects your LLM provider from these environment variables:
| Provider | Env Var | Default Model |
|----------|---------|---------------|
| OpenAI | `OPENAI_API_KEY` | `gpt-4o-mini` |
| Anthropic | `ANTHROPIC_API_KEY` | `claude-3-5-haiku-20241022` |
| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` |
| Groq | `GROQ_API_KEY` | `openai/gpt-oss-20b` |
| Provider | Env Var | Default Model | Notes |
|----------|---------|---------------|-------|
| OpenAI | `OPENAI_API_KEY` | `gpt-4o-mini` | |
| Anthropic | `ANTHROPIC_API_KEY` | `claude-3-5-haiku-20241022` | |
| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` | |
| Groq | `GROQ_API_KEY` | `openai/gpt-oss-20b` | |
| Claude Code | `HINDSIGHT_API_LLM_PROVIDER=claude-code` | `claude-sonnet-4-20250514` | No API key needed |
| OpenAI Codex | `HINDSIGHT_API_LLM_PROVIDER=openai-codex` | `o3-mini` | No API key needed |
**Override with explicit config:**
@@ -133,32 +143,32 @@ Useful for shared memory across multiple OpenClaw instances or production deploy
View the daemon config that was written by the plugin:
```bash
cat ~/.hindsight/embed
cat ~/.hindsight/profiles/openclaw.env
```
This shows the LLM provider, model, and other settings the daemon is using.
This shows the LLM provider, model, port, and other settings the daemon is using.
### Check Daemon Status
```bash
# Check if daemon is running
uvx hindsight-embed@latest daemon status
uvx hindsight-embed@latest -p openclaw daemon status
# View daemon logs
tail -f ~/.hindsight/daemon.log
tail -f ~/.hindsight/profiles/openclaw.log
```
### Query Memories
```bash
# Search memories
uvx hindsight-embed@latest memory recall openclaw "user preferences"
uvx hindsight-embed@latest -p openclaw memory recall openclaw "user preferences"
# View recent memories
uvx hindsight-embed@latest memory list openclaw --limit 10
uvx hindsight-embed@latest -p openclaw memory list openclaw --limit 10
# Open web UI
uvx hindsight-embed@latest ui
# Open web UI (uses openclaw profile's daemon)
uvx hindsight-embed@latest -p openclaw ui
```
## Troubleshooting
@@ -176,27 +186,40 @@ openclaw plugins install @vectorize-io/hindsight-openclaw
### Daemon not starting
```bash
# Check daemon status
uvx hindsight-embed@latest daemon status
# Check daemon status (note: -p openclaw uses the openclaw profile)
uvx hindsight-embed@latest -p openclaw daemon status
# View logs for errors
tail -f ~/.hindsight/daemon.log
tail -f ~/.hindsight/profiles/openclaw.log
# Check configuration
cat ~/.hindsight/embed
cat ~/.hindsight/profiles/openclaw.env
# List all profiles
uvx hindsight-embed@latest profile list
```
### No API key error
Make sure you've set one of the provider API keys:
Make sure you've set one of the provider API keys (or use a provider that doesn't require one):
```bash
# Option 1: OpenAI
export OPENAI_API_KEY="sk-your-key"
# or
# Option 2: Anthropic
export ANTHROPIC_API_KEY="your-key"
# Option 3: Claude Code (no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=claude-code
# Option 4: OpenAI Codex (no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
# Verify it's set
echo $OPENAI_API_KEY
# or
echo $HINDSIGHT_API_LLM_PROVIDER
```
### Verify it's working
@@ -208,6 +231,8 @@ tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight
# Should see on startup:
# [Hindsight] ✓ Using provider: openai, model: gpt-4o-mini
# or
# [Hindsight] ✓ Using provider: claude-code, model: claude-sonnet-4-20250514
# Should see after conversations:
# [Hindsight] Retained X messages for session ...
File diff suppressed because it is too large Load Diff
@@ -153,31 +153,29 @@ def _start_daemon(config: dict, profile: str | None = None) -> bool:
# Get idle timeout from environment or use default
idle_timeout = int(os.getenv("HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT", str(DEFAULT_DAEMON_IDLE_TIMEOUT)))
# Pass profile-specific port and lockfile
# Use profile-specific log file
daemon_log = paths.log
daemon_log.parent.mkdir(parents=True, exist_ok=True)
# Tell hindsight-api daemon where to write its logs
env["HINDSIGHT_API_DAEMON_LOG"] = str(daemon_log)
# Pass profile-specific port (no lockfile - we use port-based discovery)
cmd = _find_hindsight_api_command() + [
"--daemon",
"--idle-timeout",
str(idle_timeout),
"--port",
str(paths.port),
"--lockfile",
str(paths.lock),
]
# Use profile-specific log file
daemon_log = paths.log
daemon_log.parent.mkdir(parents=True, exist_ok=True)
print(f"Starting daemon with command: {' '.join(cmd)}", file=sys.stderr)
print(f" Log file: {daemon_log}", file=sys.stderr)
try:
# Start daemon with shell redirection (works across forks)
# The >> appends to log file, 2>&1 redirects stderr to stdout
shell_cmd = f"{' '.join(cmd)} >> {shlex.quote(str(daemon_log))} 2>&1"
# Start daemon directly (hindsight-api handles its own log redirection via HINDSIGHT_API_DAEMON_LOG)
subprocess.Popen(
shell_cmd,
shell=True,
cmd,
env=env,
start_new_session=True,
)
@@ -261,19 +259,36 @@ def stop_daemon(profile: str | None = None) -> bool:
Returns:
True if daemon stopped successfully.
"""
import subprocess
if profile is None:
profile = resolve_active_profile()
# Get profile-specific lockfile
# Check if daemon is actually running via health check
if not _is_daemon_running(profile):
logger.debug(f"Daemon not running for profile '{profile or 'default'}'")
return True
# Get profile-specific port
pm = ProfileManager()
paths = pm.resolve_profile_paths(profile)
lockfile = paths.lock
port = paths.port
# Find PID by port using lsof (works on macOS/Linux, handles stale lockfiles)
try:
result = subprocess.run(
["lsof", "-ti", f":{port}", "-sTCP:LISTEN"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0 and result.stdout.strip():
pid = int(result.stdout.strip().split()[0])
logger.debug(f"Found daemon PID {pid} on port {port}")
# Send SIGTERM
os.kill(pid, 15)
# Try to kill by PID from lockfile
if lockfile.exists():
try:
pid = int(lockfile.read_text().strip())
os.kill(pid, 15) # SIGTERM
# Wait for process to exit
for _ in range(50):
time.sleep(0.1)
@@ -281,8 +296,10 @@ def stop_daemon(profile: str | None = None) -> bool:
os.kill(pid, 0)
except OSError:
break # Process exited
except (ValueError, OSError):
pass
else:
logger.warning(f"Could not find PID for port {port}")
except (subprocess.TimeoutExpired, ValueError, OSError, FileNotFoundError) as e:
logger.warning(f"Could not find/kill daemon by port: {e}")
# Wait for health check to fail (daemon fully stopped)
for _ in range(30): # Wait up to 3 seconds
@@ -140,33 +140,50 @@ class ProfileManager:
return profile
return None
def create_profile(self, name: str, config: dict[str, str]):
def create_profile(self, name: str, port_or_config: int | dict[str, str], config: dict[str, str] | None = None):
"""Create or update a profile.
Args:
name: Profile name.
config: Configuration dict (KEY=VALUE pairs).
port_or_config: Port number (int) or configuration dict. For backward compatibility,
if this is a dict, it's treated as config and port is auto-allocated.
config: Configuration dict (KEY=VALUE pairs). Only used if port_or_config is an int.
Raises:
ValueError: If profile name is invalid.
ValueError: If profile name is invalid or port is invalid.
"""
# Handle backward compatibility - allow (name, config) or (name, port, config)
if isinstance(port_or_config, dict):
# Called with (name, config) - auto-allocate port
port = None
config = port_or_config
else:
# Called with (name, port, config)
port = port_or_config
if config is None:
raise ValueError("Config must be provided when port is specified")
if not name:
raise ValueError("Profile name cannot be empty")
if not name.replace("-", "").replace("_", "").isalnum():
raise ValueError(f"Invalid profile name '{name}'. Use alphanumeric chars, hyphens, and underscores.")
if port is not None and (port < 1024 or port > 65535):
raise ValueError(f"Invalid port {port}. Must be between 1024-65535.")
# Ensure profile directory exists
self._ensure_directories()
# Load metadata to check if profile already exists
metadata = self._load_metadata()
# Allocate port for this profile (preserve existing port if updating)
if name in metadata.profiles and "port" in metadata.profiles[name]:
port = metadata.profiles[name]["port"]
else:
port = self._allocate_port(name)
# Determine port: use provided port, preserve existing, or allocate new
if port is None:
if name in metadata.profiles and "port" in metadata.profiles[name]:
port = metadata.profiles[name]["port"]
else:
port = self._allocate_port(name)
# Write config file
config_path = PROFILES_DIR / f"{name}.env"
+8 -53
View File
@@ -2,6 +2,7 @@
#
# Smoke test for hindsight-embed CLI with daemon mode
# Tests retain and recall operations via the background daemon
# Verifies daemon lifecycle, memory retention, and recall functionality
#
set -e
@@ -16,8 +17,8 @@ if [ -f ~/.hindsight/config.env ]; then
source ~/.hindsight/config.env
fi
if [ -z "$HINDSIGHT_EMBED_LLM_API_KEY" ] && [ -z "$OPENAI_API_KEY" ]; then
echo "Error: HINDSIGHT_EMBED_LLM_API_KEY or OPENAI_API_KEY is required"
if [ -z "$HINDSIGHT_API_LLM_API_KEY" ] && [ -z "$OPENAI_API_KEY" ]; then
echo "Error: HINDSIGHT_API_LLM_API_KEY or OPENAI_API_KEY is required"
exit 1
fi
@@ -27,60 +28,14 @@ echo "Using bank ID: $BANK_ID"
echo "Script dir: $SCRIPT_DIR"
echo "API dir: $API_DIR"
# Debug: Check if hindsight CLI is available
echo ""
echo "Checking hindsight CLI availability..."
if command -v hindsight &> /dev/null; then
echo " hindsight CLI found at: $(which hindsight)"
else
echo " hindsight CLI not in PATH"
if [ -f ~/.local/bin/hindsight ]; then
echo " Found at ~/.local/bin/hindsight"
else
echo " Not found at ~/.local/bin/hindsight - attempting installation..."
# Try to install the CLI
if curl -fsSL https://hindsight.vectorize.io/get-cli | bash; then
echo " CLI installation completed"
if [ -f ~/.local/bin/hindsight ]; then
echo " CLI now available at ~/.local/bin/hindsight"
export PATH="$HOME/.local/bin:$PATH"
else
echo " WARNING: CLI still not found after installation"
ls -la ~/.local/bin/ 2>/dev/null || echo " ~/.local/bin does not exist"
fi
else
echo " WARNING: CLI installation failed"
fi
fi
fi
# Show environment info for debugging
echo ""
echo "Environment:"
echo " HINDSIGHT_EMBED_LLM_PROVIDER: ${HINDSIGHT_EMBED_LLM_PROVIDER:-not set}"
echo " HINDSIGHT_EMBED_LLM_MODEL: ${HINDSIGHT_EMBED_LLM_MODEL:-not set}"
echo " HINDSIGHT_EMBED_LLM_API_KEY: ${HINDSIGHT_EMBED_LLM_API_KEY:+set (hidden)}"
echo " PATH includes ~/.local/bin: $(echo $PATH | grep -q "$HOME/.local/bin" && echo yes || echo no)"
# Final check that CLI is available before proceeding
echo ""
echo "Final CLI check before tests..."
CLI_PATH=""
if [ -f ~/.local/bin/hindsight ]; then
CLI_PATH="$HOME/.local/bin/hindsight"
elif command -v hindsight &> /dev/null; then
CLI_PATH="$(which hindsight)"
fi
if [ -n "$CLI_PATH" ]; then
echo " CLI found at: $CLI_PATH"
echo " CLI version: $($CLI_PATH --version 2>&1 || echo 'unknown')"
else
echo " ERROR: hindsight CLI not found. Tests cannot proceed."
echo " The hindsight-embed package forwards commands to the hindsight CLI."
echo " Please ensure the CLI is installed or check the get-cli installer output above."
exit 1
fi
echo " HINDSIGHT_API_LLM_PROVIDER: ${HINDSIGHT_API_LLM_PROVIDER:-not set}"
echo " HINDSIGHT_API_LLM_MODEL: ${HINDSIGHT_API_LLM_MODEL:-not set}"
echo " HINDSIGHT_API_LLM_API_KEY: ${HINDSIGHT_API_LLM_API_KEY:+set (hidden)}"
echo " Python: $(python3 --version 2>&1)"
echo " uv: $(uv --version 2>&1)"
# Stop any existing daemon
echo ""
+42 -2
View File
@@ -5,9 +5,15 @@ Biomimetic long-term memory for [OpenClaw](https://openclaw.ai) using [Hindsight
## Quick Start
```bash
# 1. Configure your LLM provider
# 1. Configure your LLM provider for memory extraction
# Option A: OpenAI
export OPENAI_API_KEY="sk-your-key"
openclaw config set 'agents.defaults.models."openai/gpt-4o-mini"' '{}'
# Option B: Claude Code (no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=claude-code
# Option C: OpenAI Codex (no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
# 2. Install and enable the plugin
openclaw plugins install @vectorize-io/hindsight-openclaw
@@ -24,6 +30,40 @@ For full documentation, configuration options, troubleshooting, and development
**[OpenClaw Integration Documentation](https://vectorize.io/hindsight/sdks/integrations/openclaw)**
## Development
To test local changes to the Hindsight package before publishing:
1. Add `embedPackagePath` to your plugin config in `~/.openclaw/openclaw.json`:
```json
{
"plugins": {
"entries": {
"hindsight-openclaw": {
"enabled": true,
"config": {
"embedPackagePath": "/path/to/hindsight-wt3/hindsight-embed"
}
}
}
}
}
```
2. The plugin will use `uv run --directory <path> hindsight-embed` instead of `uvx hindsight-embed@latest`
3. To use a specific profile for testing:
```bash
# Check daemon status
uvx hindsight-embed@latest -p openclaw daemon status
# View logs
tail -f ~/.hindsight/profiles/openclaw.log
# List profiles
uvx hindsight-embed@latest profile list
```
## Links
- [Hindsight Documentation](https://vectorize.io/hindsight)
@@ -27,8 +27,8 @@
},
"llmProvider": {
"type": "string",
"description": "LLM provider for Hindsight memory (e.g. 'openai', 'anthropic', 'gemini', 'groq', 'ollama'). Takes priority over auto-detection but not over HINDSIGHT_API_LLM_PROVIDER env var.",
"enum": ["openai", "anthropic", "gemini", "groq", "ollama"]
"description": "LLM provider for Hindsight memory (e.g. 'openai', 'anthropic', 'gemini', 'groq', 'ollama', 'openai-codex', 'claude-code'). Takes priority over auto-detection but not over HINDSIGHT_API_LLM_PROVIDER env var.",
"enum": ["openai", "anthropic", "gemini", "groq", "ollama", "openai-codex", "claude-code"]
},
"llmModel": {
"type": "string",
@@ -37,6 +37,15 @@
"llmApiKeyEnv": {
"type": "string",
"description": "Name of the env var holding the API key (e.g. 'MY_CUSTOM_KEY'). If not set, uses the standard env var for the chosen provider."
},
"embedPackagePath": {
"type": "string",
"description": "Local path to hindsight package for development (e.g. '/path/to/hindsight'). When set, uses 'uv run --directory <path>' instead of 'uvx hindsight-embed@latest'."
},
"apiPort": {
"type": "number",
"description": "Port for the openclaw profile daemon (default: 9077)",
"default": 9077
}
},
"additionalProperties": false
@@ -69,6 +78,14 @@
"llmApiKeyEnv": {
"label": "API Key Env Var",
"placeholder": "e.g. MY_CUSTOM_API_KEY (optional)"
},
"embedPackagePath": {
"label": "Local Package Path (Dev)",
"placeholder": "/path/to/hindsight (for local development)"
},
"apiPort": {
"label": "API Port",
"placeholder": "9077 (default)"
}
}
}
+26 -24
View File
@@ -16,12 +16,28 @@ export class HindsightClient {
private llmApiKey: string;
private llmModel?: string;
private embedVersion: string;
private embedPackagePath?: string;
constructor(llmProvider: string, llmApiKey: string, llmModel?: string, embedVersion: string = 'latest') {
constructor(llmProvider: string, llmApiKey: string, llmModel?: string, embedVersion: string = 'latest', embedPackagePath?: string) {
this.llmProvider = llmProvider;
this.llmApiKey = llmApiKey;
this.llmModel = llmModel;
this.embedVersion = embedVersion || 'latest';
this.embedPackagePath = embedPackagePath;
}
/**
* Get the command prefix to run hindsight-embed (either local or from PyPI)
*/
private getEmbedCommandPrefix(): string {
if (this.embedPackagePath) {
// Local package: uv run --directory <path> hindsight-embed
return `uv run --directory ${this.embedPackagePath} hindsight-embed`;
} else {
// PyPI package: uvx hindsight-embed@version
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
return `uvx ${embedPackage}`;
}
}
setBankId(bankId: string): void {
@@ -34,11 +50,11 @@ export class HindsightClient {
}
const escapedMission = mission.replace(/'/g, "'\\''"); // Escape single quotes
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
const cmd = `uvx ${embedPackage} bank mission ${this.bankId} '${escapedMission}'`;
const embedCmd = this.getEmbedCommandPrefix();
const cmd = `${embedCmd} --profile openclaw bank mission ${this.bankId} '${escapedMission}'`;
try {
const { stdout } = await execAsync(cmd, { env: this.getEnv() });
const { stdout } = await execAsync(cmd);
console.log(`[Hindsight] Bank mission set: ${stdout.trim()}`);
} catch (error) {
// Don't fail if mission set fails - bank might not exist yet, will be created on first retain
@@ -46,29 +62,15 @@ export class HindsightClient {
}
}
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 embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
const cmd = `uvx ${embedPackage} memory retain ${this.bankId} '${content}' --doc-id '${docId}' --async`;
const embedCmd = this.getEmbedCommandPrefix();
const cmd = `${embedCmd} --profile openclaw memory retain ${this.bankId} '${content}' --doc-id '${docId}' --async`;
try {
const { stdout } = await execAsync(cmd, { env: this.getEnv() });
const { stdout } = await execAsync(cmd);
console.log(`[Hindsight] Retained (async): ${stdout.trim()}`);
// Return a simple response
@@ -86,11 +88,11 @@ export class HindsightClient {
const query = request.query.replace(/'/g, "'\\''"); // Escape single quotes
const maxTokens = request.max_tokens || 1024;
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
const cmd = `uvx ${embedPackage} memory recall ${this.bankId} '${query}' --output json --max-tokens ${maxTokens}`;
const embedCmd = this.getEmbedCommandPrefix();
const cmd = `${embedCmd} --profile openclaw memory recall ${this.bankId} '${query}' --output json --max-tokens ${maxTokens}`;
try {
const { stdout } = await execAsync(cmd, { env: this.getEnv() });
const { stdout } = await execAsync(cmd);
// Parse JSON output - returns { entities: {...}, results: [...] }
const response = JSON.parse(stdout);
@@ -15,6 +15,7 @@ export class HindsightEmbedManager {
private llmBaseUrl?: string;
private daemonIdleTimeout: number;
private embedVersion: string;
private embedPackagePath?: string;
constructor(
port: number,
@@ -23,10 +24,12 @@ export class HindsightEmbedManager {
llmModel?: string,
llmBaseUrl?: string,
daemonIdleTimeout: number = 0, // Default: never timeout
embedVersion: string = 'latest' // Default: latest
embedVersion: string = 'latest', // Default: latest
embedPackagePath?: string // Local path to hindsight package
) {
this.port = 8888; // hindsight-embed daemon uses same port as API
this.baseUrl = `http://127.0.0.1:8888`;
// Use the configured port (default: 9077 from config)
this.port = port;
this.baseUrl = `http://127.0.0.1:${port}`;
this.embedDir = join(homedir(), '.openclaw', 'hindsight-embed');
this.llmProvider = llmProvider;
this.llmApiKey = llmApiKey;
@@ -34,21 +37,36 @@ export class HindsightEmbedManager {
this.llmBaseUrl = llmBaseUrl;
this.daemonIdleTimeout = daemonIdleTimeout;
this.embedVersion = embedVersion || 'latest';
this.embedPackagePath = embedPackagePath;
}
/**
* Get the command to run hindsight-embed (either local or from PyPI)
*/
private getEmbedCommand(): string[] {
if (this.embedPackagePath) {
// Local package: uv run --directory <path> hindsight-embed
return ['uv', 'run', '--directory', this.embedPackagePath, 'hindsight-embed'];
} else {
// PyPI package: uvx hindsight-embed@version
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
return ['uvx', embedPackage];
}
}
async start(): Promise<void> {
console.log(`[Hindsight] Starting hindsight-embed daemon...`);
// Build environment variables
// Build environment variables using standard HINDSIGHT_API_LLM_* variables
const env: NodeJS.ProcessEnv = {
...process.env,
HINDSIGHT_EMBED_LLM_PROVIDER: this.llmProvider,
HINDSIGHT_EMBED_LLM_API_KEY: this.llmApiKey,
HINDSIGHT_API_LLM_PROVIDER: this.llmProvider,
HINDSIGHT_API_LLM_API_KEY: this.llmApiKey,
HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT: this.daemonIdleTimeout.toString(),
};
if (this.llmModel) {
env['HINDSIGHT_EMBED_LLM_MODEL'] = this.llmModel;
env['HINDSIGHT_API_LLM_MODEL'] = this.llmModel;
}
// Pass through base URL for OpenAI-compatible providers (OpenRouter, etc.)
@@ -62,16 +80,16 @@ export class HindsightEmbedManager {
env['HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU'] = '1';
}
// Write env vars to ~/.hindsight/config.env for daemon persistence
await this.writeConfigEnv(env);
// Configure "openclaw" profile using hindsight-embed configure (non-interactive)
console.log('[Hindsight] Configuring "openclaw" profile...');
await this.configureProfile(env);
// Start hindsight-embed daemon (it manages itself)
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
// Start hindsight-embed daemon with openclaw profile
const embedCmd = this.getEmbedCommand();
const startDaemon = spawn(
'uvx',
[embedPackage, 'daemon', 'start'],
embedCmd[0],
[...embedCmd.slice(1), 'daemon', '--profile', 'openclaw', 'start'],
{
env,
stdio: 'pipe',
}
);
@@ -114,8 +132,8 @@ export class HindsightEmbedManager {
async stop(): Promise<void> {
console.log('[Hindsight] Stopping hindsight-embed daemon...');
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
const stopDaemon = spawn('uvx', [embedPackage, 'daemon', 'stop'], {
const embedCmd = this.getEmbedCommand();
const stopDaemon = spawn(embedCmd[0], [...embedCmd.slice(1), 'daemon', '--profile', 'openclaw', 'stop'], {
stdio: 'pipe',
});
@@ -172,75 +190,60 @@ export class HindsightEmbedManager {
}
}
private async writeConfigEnv(env: NodeJS.ProcessEnv): Promise<void> {
const hindsightDir = join(homedir(), '.hindsight');
const embedConfigPath = join(hindsightDir, 'embed');
private async configureProfile(env: NodeJS.ProcessEnv): Promise<void> {
// Build profile create command args with --merge, --port and --env flags
// Use --merge to allow updating existing profile
const createArgs = ['profile', 'create', 'openclaw', '--merge', '--port', this.port.toString()];
// Ensure directory exists
await fs.mkdir(hindsightDir, { recursive: true });
// Read existing config to preserve extra settings
let existingContent = '';
let extraSettings: string[] = [];
try {
existingContent = await fs.readFile(embedConfigPath, 'utf-8');
// Extract non-LLM settings (like FORCE_CPU flags)
const lines = existingContent.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#') &&
!trimmed.startsWith('HINDSIGHT_EMBED_LLM_') &&
!trimmed.startsWith('HINDSIGHT_API_LLM_') &&
!trimmed.startsWith('HINDSIGHT_EMBED_BANK_ID') &&
!trimmed.startsWith('HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT')) {
extraSettings.push(line);
}
}
} catch {
// File doesn't exist yet, that's fine
}
// Build config file with header
const configLines: string[] = [
'# Hindsight Embed Configuration',
'# Generated by OpenClaw Hindsight plugin',
'',
// Add all environment variables as --env flags
const envVars = [
'HINDSIGHT_API_LLM_PROVIDER',
'HINDSIGHT_API_LLM_MODEL',
'HINDSIGHT_API_LLM_API_KEY',
'HINDSIGHT_API_LLM_BASE_URL',
'HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT',
'HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU',
'HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU',
];
// Add LLM config
if (env.HINDSIGHT_EMBED_LLM_PROVIDER) {
configLines.push(`HINDSIGHT_EMBED_LLM_PROVIDER=${env.HINDSIGHT_EMBED_LLM_PROVIDER}`);
}
if (env.HINDSIGHT_EMBED_LLM_MODEL) {
configLines.push(`HINDSIGHT_EMBED_LLM_MODEL=${env.HINDSIGHT_EMBED_LLM_MODEL}`);
}
if (env.HINDSIGHT_EMBED_LLM_API_KEY) {
configLines.push(`HINDSIGHT_EMBED_LLM_API_KEY=${env.HINDSIGHT_EMBED_LLM_API_KEY}`);
}
if (env.HINDSIGHT_API_LLM_BASE_URL) {
configLines.push(`HINDSIGHT_API_LLM_BASE_URL=${env.HINDSIGHT_API_LLM_BASE_URL}`);
}
if (env.HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT) {
configLines.push(`HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT=${env.HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT}`);
for (const envVar of envVars) {
if (env[envVar]) {
createArgs.push('--env', `${envVar}=${env[envVar]}`);
}
}
// Add platform-specific config (macOS FORCE_CPU flags)
if (env.HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU) {
configLines.push(`HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU=${env.HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU}`);
}
if (env.HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU) {
configLines.push(`HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=${env.HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU}`);
}
// Run profile create command (non-interactive, overwrites if exists)
const embedCmd = this.getEmbedCommand();
const create = spawn(embedCmd[0], [...embedCmd.slice(1), ...createArgs], {
stdio: 'pipe',
});
// Add extra settings if they exist
if (extraSettings.length > 0) {
configLines.push('');
configLines.push('# Additional settings');
configLines.push(...extraSettings);
}
let output = '';
create.stdout?.on('data', (data) => {
const text = data.toString();
output += text;
console.log(`[Hindsight] ${text.trim()}`);
});
// Write to file
await fs.writeFile(embedConfigPath, configLines.join('\n') + '\n', 'utf-8');
console.log(`[Hindsight] Wrote config to ${embedConfigPath}`);
create.stderr?.on('data', (data) => {
const text = data.toString();
output += text;
console.error(`[Hindsight] ${text.trim()}`);
});
await new Promise<void>((resolve, reject) => {
create.on('exit', (code) => {
if (code === 0) {
console.log('[Hindsight] Profile "openclaw" configured successfully');
resolve();
} else {
reject(new Error(`Profile create failed with code ${code}: ${output}`));
}
});
create.on('error', (error) => {
reject(error);
});
});
}
}
+31 -18
View File
@@ -35,6 +35,8 @@ const PROVIDER_DETECTION = [
{ name: 'gemini', keyEnv: 'GEMINI_API_KEY', defaultModel: 'gemini-2.5-flash' },
{ name: 'groq', keyEnv: 'GROQ_API_KEY', defaultModel: 'openai/gpt-oss-20b' },
{ name: 'ollama', keyEnv: '', defaultModel: 'llama3.2' },
{ name: 'openai-codex', keyEnv: '', defaultModel: 'gpt-5.2-codex' },
{ name: 'claude-code', keyEnv: '', defaultModel: 'claude-sonnet-4-5-20250929' },
];
function detectLLMConfig(pluginConfig?: PluginConfig): {
@@ -52,7 +54,9 @@ function detectLLMConfig(pluginConfig?: PluginConfig): {
// Priority 1: If provider is explicitly set via env var, use that
if (overrideProvider) {
if (!overrideKey && overrideProvider !== 'ollama') {
// Providers that don't require an API key (use OAuth or local models)
const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
if (!overrideKey && !noKeyRequired.includes(overrideProvider)) {
throw new Error(
`HINDSIGHT_API_LLM_PROVIDER is set to "${overrideProvider}" but HINDSIGHT_API_LLM_API_KEY is not set.\n` +
`Please set: export HINDSIGHT_API_LLM_API_KEY=your-api-key`
@@ -81,7 +85,9 @@ function detectLLMConfig(pluginConfig?: PluginConfig): {
apiKey = process.env[providerInfo.keyEnv] || '';
}
if (!apiKey && pluginConfig.llmProvider !== 'ollama') {
// Providers that don't require an API key (use OAuth or local models)
const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
if (!apiKey && !noKeyRequired.includes(pluginConfig.llmProvider)) {
const keySource = pluginConfig.llmApiKeyEnv || providerInfo?.keyEnv || 'unknown';
throw new Error(
`Plugin config llmProvider is set to "${pluginConfig.llmProvider}" but no API key found.\n` +
@@ -103,8 +109,9 @@ function detectLLMConfig(pluginConfig?: PluginConfig): {
for (const providerInfo of PROVIDER_DETECTION) {
const apiKey = providerInfo.keyEnv ? process.env[providerInfo.keyEnv] : '';
// Skip ollama in auto-detection (must be explicitly requested)
if (providerInfo.name === 'ollama') {
// Skip providers that don't use API keys in auto-detection (must be explicitly requested)
const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
if (noKeyRequired.includes(providerInfo.name)) {
continue;
}
@@ -125,11 +132,14 @@ function detectLLMConfig(pluginConfig?: PluginConfig): {
`Option 1: Set a standard provider API key (auto-detect):\n` +
` export OPENAI_API_KEY=sk-your-key # Uses gpt-4o-mini\n` +
` export ANTHROPIC_API_KEY=your-key # Uses claude-3-5-haiku\n` +
` export GEMINI_API_KEY=your-key # Uses gemini-2.0-flash-exp\n` +
` export GROQ_API_KEY=your-key # Uses llama-3.3-70b-versatile\n\n` +
`Option 2: Set llmProvider in openclaw.json plugin config:\n` +
` export GEMINI_API_KEY=your-key # Uses gemini-2.5-flash\n` +
` export GROQ_API_KEY=your-key # Uses openai/gpt-oss-20b\n\n` +
`Option 2: Use Codex or Claude Code (no API key needed):\n` +
` export HINDSIGHT_API_LLM_PROVIDER=openai-codex # Requires 'codex auth login'\n` +
` export HINDSIGHT_API_LLM_PROVIDER=claude-code # Requires Claude Code CLI\n\n` +
`Option 3: Set llmProvider in openclaw.json plugin config:\n` +
` "llmProvider": "openai", "llmModel": "gpt-4o-mini"\n\n` +
`Option 3: Override with Hindsight-specific env vars:\n` +
`Option 4: Override with Hindsight-specific env vars:\n` +
` export HINDSIGHT_API_LLM_PROVIDER=openai\n` +
` export HINDSIGHT_API_LLM_MODEL=gpt-4o-mini\n` +
` export HINDSIGHT_API_LLM_API_KEY=sk-your-key\n` +
@@ -147,6 +157,7 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
embedPort: config.embedPort || 0,
daemonIdleTimeout: config.daemonIdleTimeout !== undefined ? config.daemonIdleTimeout : 0,
embedVersion: config.embedVersion || 'latest',
embedPackagePath: config.embedPackagePath,
llmProvider: config.llmProvider,
llmModel: config.llmModel,
llmApiKeyEnv: config.llmApiKeyEnv,
@@ -178,9 +189,9 @@ export default function (api: MoltbotPluginAPI) {
}
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}`);
// Get API port from config (default: 9077)
const apiPort = pluginConfig.apiPort || 9077;
console.log(`[Hindsight] API Port: ${apiPort}`);
// Initialize in background (non-blocking)
console.log('[Hindsight] Starting initialization in background...');
@@ -189,13 +200,14 @@ export default function (api: MoltbotPluginAPI) {
// Initialize embed manager
console.log('[Hindsight] Creating HindsightEmbedManager...');
embedManager = new HindsightEmbedManager(
port,
apiPort,
llmConfig.provider,
llmConfig.apiKey,
llmConfig.model,
llmConfig.baseUrl,
pluginConfig.daemonIdleTimeout,
pluginConfig.embedVersion
pluginConfig.embedVersion,
pluginConfig.embedPackagePath
);
// Start the embedded server
@@ -204,7 +216,7 @@ export default function (api: MoltbotPluginAPI) {
// Initialize client
console.log('[Hindsight] Creating HindsightClient...');
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion);
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
// Use openclaw bank
console.log(`[Hindsight] Using bank: ${BANK_NAME}`);
@@ -263,21 +275,22 @@ export default function (api: MoltbotPluginAPI) {
console.log('[Hindsight] Reinitializing daemon...');
const pluginConfig = getPluginConfig(api);
const llmConfig = detectLLMConfig(pluginConfig);
const port = pluginConfig.embedPort || Math.floor(Math.random() * 10000) + 10000;
const apiPort = pluginConfig.apiPort || 9077;
embedManager = new HindsightEmbedManager(
port,
apiPort,
llmConfig.provider,
llmConfig.apiKey,
llmConfig.model,
llmConfig.baseUrl,
pluginConfig.daemonIdleTimeout,
pluginConfig.embedVersion
pluginConfig.embedVersion,
pluginConfig.embedPackagePath
);
await embedManager.start();
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion);
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
client.setBankId(BANK_NAME);
if (pluginConfig.bankMission) {
@@ -32,9 +32,11 @@ export interface PluginConfig {
embedPort?: number;
daemonIdleTimeout?: number; // Seconds before daemon shuts down (0 = never)
embedVersion?: string; // hindsight-embed version (default: "latest")
embedPackagePath?: string; // Local path to hindsight package (e.g. '/path/to/hindsight')
llmProvider?: string; // LLM provider override (e.g. 'openai', 'anthropic', 'gemini', 'groq', 'ollama')
llmModel?: string; // LLM model override (e.g. 'gpt-4o-mini', 'claude-3-5-haiku-20241022')
llmApiKeyEnv?: string; // Env var name holding the API key (e.g. 'MY_CUSTOM_KEY')
apiPort?: number; // Port for openclaw profile daemon (default: 9077)
}
export interface ServiceConfig {