From 6b346925e2353fa41acf71a0dfb6d98e61aa11d4 Mon Sep 17 00:00:00 2001 From: Anton Evseev <78427278+slayoffer@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:24:33 +1000 Subject: [PATCH] feat(openclaw): add external Hindsight API support (#289) Add support for connecting to an external Hindsight API instead of starting a local daemon. This enables: - Shared memory across multiple OpenClaw instances - Centralized Hindsight deployment (e.g., on GKE) - Reduced resource usage (no local daemon per instance) Configuration: - HINDSIGHT_EMBED_API_URL env var or hindsightApiUrl in plugin config - HINDSIGHT_EMBED_API_TOKEN env var or hindsightApiToken for auth When external API is configured: - Skip local daemon startup - Health check external API on startup - Pass API URL/token to CLI commands via env vars Falls back to local daemon mode when not configured. --- .../openclaw/openclaw.plugin.json | 17 ++ hindsight-integrations/openclaw/src/index.ts | 246 +++++++++++++----- hindsight-integrations/openclaw/src/types.ts | 2 + 3 files changed, 199 insertions(+), 66 deletions(-) diff --git a/hindsight-integrations/openclaw/openclaw.plugin.json b/hindsight-integrations/openclaw/openclaw.plugin.json index f6612366..4bfa1242 100644 --- a/hindsight-integrations/openclaw/openclaw.plugin.json +++ b/hindsight-integrations/openclaw/openclaw.plugin.json @@ -46,6 +46,15 @@ "type": "number", "description": "Port for the openclaw profile daemon (default: 9077)", "default": 9077 + }, + "hindsightApiUrl": { + "type": "string", + "description": "External Hindsight API URL (e.g. 'https://mcp.hindsight.devcraft.team'). When set, skips local daemon and connects directly to this API.", + "format": "uri" + }, + "hindsightApiToken": { + "type": "string", + "description": "API token for external Hindsight API authentication. Required if the external API has authentication enabled." } }, "additionalProperties": false @@ -86,6 +95,14 @@ "apiPort": { "label": "API Port", "placeholder": "9077 (default)" + }, + "hindsightApiUrl": { + "label": "External Hindsight API URL", + "placeholder": "e.g. https://mcp.hindsight.devcraft.team (leave empty for local daemon)" + }, + "hindsightApiToken": { + "label": "External API Token", + "placeholder": "API token if external API requires authentication" } } } diff --git a/hindsight-integrations/openclaw/src/index.ts b/hindsight-integrations/openclaw/src/index.ts index 938eb5ef..7996088b 100644 --- a/hindsight-integrations/openclaw/src/index.ts +++ b/hindsight-integrations/openclaw/src/index.ts @@ -9,6 +9,7 @@ let embedManager: HindsightEmbedManager | null = null; let client: HindsightClient | null = null; let initPromise: Promise | null = null; let isInitialized = false; +let usingExternalApi = false; // Track if using external API (skip daemon management) // Global access for hooks (Moltbot loads hooks separately) if (typeof global !== 'undefined') { @@ -148,6 +149,37 @@ function detectLLMConfig(pluginConfig?: PluginConfig): { ); } +/** + * Detect external Hindsight API configuration. + * Priority: env vars > plugin config + */ +function detectExternalApi(pluginConfig?: PluginConfig): { + apiUrl: string | null; + apiToken: string | null; +} { + const apiUrl = process.env.HINDSIGHT_EMBED_API_URL || pluginConfig?.hindsightApiUrl || null; + const apiToken = process.env.HINDSIGHT_EMBED_API_TOKEN || pluginConfig?.hindsightApiToken || null; + return { apiUrl, apiToken }; +} + +/** + * Health check for external Hindsight API. + */ +async function checkExternalApiHealth(apiUrl: string): Promise { + const healthUrl = `${apiUrl.replace(/\/$/, '')}/health`; + console.log(`[Hindsight] Checking external API health at ${healthUrl}...`); + try { + const response = await fetch(healthUrl, { signal: AbortSignal.timeout(10000) }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + const data = await response.json() as { status?: string }; + console.log(`[Hindsight] External API health: ${JSON.stringify(data)}`); + } catch (error) { + throw new Error(`Cannot connect to external Hindsight API at ${apiUrl}: ${error}`); + } +} + function getPluginConfig(api: MoltbotPluginAPI): PluginConfig { const config = api.config.plugins?.entries?.['hindsight-openclaw']?.config || {}; const defaultMission = 'You are an AI assistant helping users across multiple communication channels (Telegram, Slack, Discord, etc.). Remember user preferences, instructions, and important context from conversations to provide personalized assistance.'; @@ -161,6 +193,8 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig { llmProvider: config.llmProvider, llmModel: config.llmModel, llmApiKeyEnv: config.llmApiKeyEnv, + hindsightApiUrl: config.hindsightApiUrl, + hindsightApiToken: config.hindsightApiToken, }; } @@ -187,49 +221,88 @@ export default function (api: MoltbotPluginAPI) { 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)`); + // Detect external API mode + const externalApi = detectExternalApi(pluginConfig); // Get API port from config (default: 9077) const apiPort = pluginConfig.apiPort || 9077; - console.log(`[Hindsight] API Port: ${apiPort}`); + + if (externalApi.apiUrl) { + // External API mode - skip local daemon + usingExternalApi = true; + console.log(`[Hindsight] ✓ Using external API: ${externalApi.apiUrl}`); + + // Set env vars so CLI commands (uvx hindsight-embed) use external API + process.env.HINDSIGHT_EMBED_API_URL = externalApi.apiUrl; + if (externalApi.apiToken) { + process.env.HINDSIGHT_EMBED_API_TOKEN = externalApi.apiToken; + console.log('[Hindsight] API token configured'); + } + } else { + console.log(`[Hindsight] Daemon idle timeout: ${pluginConfig.daemonIdleTimeout}s (0 = never timeout)`); + console.log(`[Hindsight] API Port: ${apiPort}`); + } // Initialize in background (non-blocking) console.log('[Hindsight] Starting initialization in background...'); initPromise = (async () => { try { - // Initialize embed manager - console.log('[Hindsight] Creating HindsightEmbedManager...'); - embedManager = new HindsightEmbedManager( - apiPort, - llmConfig.provider, - llmConfig.apiKey, - llmConfig.model, - llmConfig.baseUrl, - pluginConfig.daemonIdleTimeout, - pluginConfig.embedVersion, - pluginConfig.embedPackagePath - ); + if (usingExternalApi && externalApi.apiUrl) { + // External API mode - check health, skip daemon startup + console.log('[Hindsight] External API mode - skipping local daemon...'); + await checkExternalApiHealth(externalApi.apiUrl); - // Start the embedded server - console.log('[Hindsight] Starting embedded server...'); - await embedManager.start(); + // Initialize client (CLI commands will use external API via env vars) + console.log('[Hindsight] Creating HindsightClient...'); + client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath); - // Initialize client - console.log('[Hindsight] Creating HindsightClient...'); - client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath); + // Use openclaw bank + console.log(`[Hindsight] Using bank: ${BANK_NAME}`); + client.setBankId(BANK_NAME); - // Use openclaw bank - console.log(`[Hindsight] Using bank: ${BANK_NAME}`); - client.setBankId(BANK_NAME); + // Set bank mission + if (pluginConfig.bankMission) { + console.log(`[Hindsight] Setting bank mission...`); + await client.setBankMission(pluginConfig.bankMission); + } - // Set bank mission - if (pluginConfig.bankMission) { - console.log(`[Hindsight] Setting bank mission...`); - await client.setBankMission(pluginConfig.bankMission); + isInitialized = true; + console.log('[Hindsight] ✓ Ready (external API mode)'); + } else { + // Local daemon mode - start hindsight-embed daemon + console.log('[Hindsight] Creating HindsightEmbedManager...'); + embedManager = new HindsightEmbedManager( + apiPort, + llmConfig.provider, + llmConfig.apiKey, + llmConfig.model, + llmConfig.baseUrl, + pluginConfig.daemonIdleTimeout, + pluginConfig.embedVersion, + pluginConfig.embedPackagePath + ); + + // 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, pluginConfig.embedVersion, pluginConfig.embedPackagePath); + + // Use openclaw bank + console.log(`[Hindsight] Using bank: ${BANK_NAME}`); + client.setBankId(BANK_NAME); + + // Set bank mission + if (pluginConfig.bankMission) { + console.log(`[Hindsight] Setting bank mission...`); + await client.setBankMission(pluginConfig.bankMission); + } + + isInitialized = true; + console.log('[Hindsight] ✓ Ready'); } - - isInitialized = true; - console.log('[Hindsight] ✓ Ready'); } catch (error) { console.error('[Hindsight] Initialization error:', error); throw error; @@ -243,7 +316,7 @@ export default function (api: MoltbotPluginAPI) { api.registerService({ id: 'hindsight-memory', async start() { - console.log('[Hindsight] Service start called - checking daemon health...'); + console.log('[Hindsight] Service start called...'); // Wait for background init if still pending if (initPromise) { @@ -255,50 +328,90 @@ export default function (api: MoltbotPluginAPI) { } } - // Check if daemon is actually healthy (handles SIGUSR1 restart case) - if (embedManager && isInitialized) { - const healthy = await embedManager.checkHealth(); - if (healthy) { - console.log('[Hindsight] Daemon is healthy'); - return; + // External API mode: check external API health + if (usingExternalApi) { + const externalApi = detectExternalApi(pluginConfig); + if (externalApi.apiUrl && isInitialized) { + try { + await checkExternalApiHealth(externalApi.apiUrl); + console.log('[Hindsight] External API is healthy'); + return; + } catch (error) { + console.error('[Hindsight] External API health check failed:', error); + // Reset state for reinitialization attempt + client = null; + isInitialized = false; + } } + } else { + // Local daemon mode: check daemon health (handles SIGUSR1 restart case) + if (embedManager && isInitialized) { + const healthy = await embedManager.checkHealth(); + if (healthy) { + console.log('[Hindsight] Daemon is healthy'); + return; + } - console.log('[Hindsight] Daemon is not responding - reinitializing...'); - // Reset state for reinitialization - embedManager = null; - client = null; - isInitialized = false; + console.log('[Hindsight] Daemon is not responding - reinitializing...'); + // Reset state for reinitialization + embedManager = null; + client = null; + isInitialized = false; + } } - // Reinitialize if needed (fresh start or recovery from dead daemon) + // Reinitialize if needed (fresh start or recovery) if (!isInitialized) { - console.log('[Hindsight] Reinitializing daemon...'); - const pluginConfig = getPluginConfig(api); - const llmConfig = detectLLMConfig(pluginConfig); - const apiPort = pluginConfig.apiPort || 9077; + console.log('[Hindsight] Reinitializing...'); + const reinitPluginConfig = getPluginConfig(api); + const llmConfig = detectLLMConfig(reinitPluginConfig); + const externalApi = detectExternalApi(reinitPluginConfig); + const apiPort = reinitPluginConfig.apiPort || 9077; - embedManager = new HindsightEmbedManager( - apiPort, - llmConfig.provider, - llmConfig.apiKey, - llmConfig.model, - llmConfig.baseUrl, - pluginConfig.daemonIdleTimeout, - pluginConfig.embedVersion, - pluginConfig.embedPackagePath - ); + if (externalApi.apiUrl) { + // External API mode + usingExternalApi = true; + process.env.HINDSIGHT_EMBED_API_URL = externalApi.apiUrl; + if (externalApi.apiToken) { + process.env.HINDSIGHT_EMBED_API_TOKEN = externalApi.apiToken; + } - await embedManager.start(); + await checkExternalApiHealth(externalApi.apiUrl); - client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath); - client.setBankId(BANK_NAME); + client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, reinitPluginConfig.embedVersion, reinitPluginConfig.embedPackagePath); + client.setBankId(BANK_NAME); - if (pluginConfig.bankMission) { - await client.setBankMission(pluginConfig.bankMission); + if (reinitPluginConfig.bankMission) { + await client.setBankMission(reinitPluginConfig.bankMission); + } + + isInitialized = true; + console.log('[Hindsight] Reinitialization complete (external API mode)'); + } else { + // Local daemon mode + embedManager = new HindsightEmbedManager( + apiPort, + llmConfig.provider, + llmConfig.apiKey, + llmConfig.model, + llmConfig.baseUrl, + reinitPluginConfig.daemonIdleTimeout, + reinitPluginConfig.embedVersion, + reinitPluginConfig.embedPackagePath + ); + + await embedManager.start(); + + client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, reinitPluginConfig.embedVersion, reinitPluginConfig.embedPackagePath); + client.setBankId(BANK_NAME); + + if (reinitPluginConfig.bankMission) { + await client.setBankMission(reinitPluginConfig.bankMission); + } + + isInitialized = true; + console.log('[Hindsight] Reinitialization complete'); } - - isInitialized = true; - console.log('[Hindsight] Reinitialization complete'); } }, @@ -306,7 +419,8 @@ export default function (api: MoltbotPluginAPI) { try { console.log('[Hindsight] Service stopping...'); - if (embedManager) { + // Only stop daemon if in local mode + if (!usingExternalApi && embedManager) { await embedManager.stop(); embedManager = null; } diff --git a/hindsight-integrations/openclaw/src/types.ts b/hindsight-integrations/openclaw/src/types.ts index e56744a4..e23912b5 100644 --- a/hindsight-integrations/openclaw/src/types.ts +++ b/hindsight-integrations/openclaw/src/types.ts @@ -37,6 +37,8 @@ export interface PluginConfig { 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) + hindsightApiUrl?: string; // External Hindsight API URL (skips local daemon when set) + hindsightApiToken?: string; // API token for external Hindsight API authentication } export interface ServiceConfig {