@vectorize-io/hindsight-openclaw 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,14 +6,16 @@ export declare class HindsightEmbedManager {
6
6
  private llmProvider;
7
7
  private llmApiKey;
8
8
  private llmModel?;
9
+ private llmBaseUrl?;
9
10
  private daemonIdleTimeout;
10
11
  private embedVersion;
11
- constructor(port: number, llmProvider: string, llmApiKey: string, llmModel?: string, daemonIdleTimeout?: number, // Default: never timeout
12
+ constructor(port: number, llmProvider: string, llmApiKey: string, llmModel?: string, llmBaseUrl?: string, daemonIdleTimeout?: number, // Default: never timeout
12
13
  embedVersion?: string);
13
14
  start(): Promise<void>;
14
15
  stop(): Promise<void>;
15
16
  private waitForReady;
16
17
  getBaseUrl(): string;
17
18
  isRunning(): boolean;
19
+ checkHealth(): Promise<boolean>;
18
20
  private writeConfigEnv;
19
21
  }
@@ -10,17 +10,19 @@ export class HindsightEmbedManager {
10
10
  llmProvider;
11
11
  llmApiKey;
12
12
  llmModel;
13
+ llmBaseUrl;
13
14
  daemonIdleTimeout;
14
15
  embedVersion;
15
- constructor(port, llmProvider, llmApiKey, llmModel, daemonIdleTimeout = 0, // Default: never timeout
16
+ constructor(port, llmProvider, llmApiKey, llmModel, llmBaseUrl, daemonIdleTimeout = 0, // Default: never timeout
16
17
  embedVersion = 'latest' // Default: latest
17
18
  ) {
18
- this.port = 8889; // hindsight-embed uses fixed port 8889
19
- this.baseUrl = `http://127.0.0.1:8889`;
19
+ this.port = 8888; // hindsight-embed daemon uses same port as API
20
+ this.baseUrl = `http://127.0.0.1:8888`;
20
21
  this.embedDir = join(homedir(), '.openclaw', 'hindsight-embed');
21
22
  this.llmProvider = llmProvider;
22
23
  this.llmApiKey = llmApiKey;
23
24
  this.llmModel = llmModel;
25
+ this.llmBaseUrl = llmBaseUrl;
24
26
  this.daemonIdleTimeout = daemonIdleTimeout;
25
27
  this.embedVersion = embedVersion || 'latest';
26
28
  }
@@ -36,6 +38,15 @@ export class HindsightEmbedManager {
36
38
  if (this.llmModel) {
37
39
  env['HINDSIGHT_EMBED_LLM_MODEL'] = this.llmModel;
38
40
  }
41
+ // Pass through base URL for OpenAI-compatible providers (OpenRouter, etc.)
42
+ if (this.llmBaseUrl) {
43
+ env['HINDSIGHT_API_LLM_BASE_URL'] = this.llmBaseUrl;
44
+ }
45
+ // On macOS, force CPU for embeddings/reranker to avoid MPS/Metal issues in daemon mode
46
+ if (process.platform === 'darwin') {
47
+ env['HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU'] = '1';
48
+ env['HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU'] = '1';
49
+ }
39
50
  // Write env vars to ~/.hindsight/config.env for daemon persistence
40
51
  await this.writeConfigEnv(env);
41
52
  // Start hindsight-embed daemon (it manages itself)
@@ -120,6 +131,15 @@ export class HindsightEmbedManager {
120
131
  isRunning() {
121
132
  return this.process !== null;
122
133
  }
134
+ async checkHealth() {
135
+ try {
136
+ const response = await fetch(`${this.baseUrl}/health`, { signal: AbortSignal.timeout(2000) });
137
+ return response.ok;
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ }
123
143
  async writeConfigEnv(env) {
124
144
  const hindsightDir = join(homedir(), '.hindsight');
125
145
  const embedConfigPath = join(hindsightDir, 'embed');
@@ -136,6 +156,7 @@ export class HindsightEmbedManager {
136
156
  const trimmed = line.trim();
137
157
  if (trimmed && !trimmed.startsWith('#') &&
138
158
  !trimmed.startsWith('HINDSIGHT_EMBED_LLM_') &&
159
+ !trimmed.startsWith('HINDSIGHT_API_LLM_') &&
139
160
  !trimmed.startsWith('HINDSIGHT_EMBED_BANK_ID') &&
140
161
  !trimmed.startsWith('HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT')) {
141
162
  extraSettings.push(line);
@@ -161,9 +182,19 @@ export class HindsightEmbedManager {
161
182
  if (env.HINDSIGHT_EMBED_LLM_API_KEY) {
162
183
  configLines.push(`HINDSIGHT_EMBED_LLM_API_KEY=${env.HINDSIGHT_EMBED_LLM_API_KEY}`);
163
184
  }
185
+ if (env.HINDSIGHT_API_LLM_BASE_URL) {
186
+ configLines.push(`HINDSIGHT_API_LLM_BASE_URL=${env.HINDSIGHT_API_LLM_BASE_URL}`);
187
+ }
164
188
  if (env.HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT) {
165
189
  configLines.push(`HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT=${env.HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT}`);
166
190
  }
191
+ // Add platform-specific config (macOS FORCE_CPU flags)
192
+ if (env.HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU) {
193
+ configLines.push(`HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU=${env.HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU}`);
194
+ }
195
+ if (env.HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU) {
196
+ configLines.push(`HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=${env.HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU}`);
197
+ }
167
198
  // Add extra settings if they exist
168
199
  if (extraSettings.length > 0) {
169
200
  configLines.push('');
package/dist/index.js CHANGED
@@ -24,69 +24,65 @@ const __filename = fileURLToPath(import.meta.url);
24
24
  const __dirname = dirname(__filename);
25
25
  // Default bank name
26
26
  const BANK_NAME = 'openclaw';
27
- // Provider mapping: moltbot provider name -> hindsight provider name
28
- const PROVIDER_MAP = {
29
- anthropic: 'anthropic',
30
- openai: 'openai',
31
- 'openai-codex': 'openai',
32
- gemini: 'gemini',
33
- groq: 'groq',
34
- ollama: 'ollama',
35
- };
36
- // Environment variable mapping
37
- const ENV_KEY_MAP = {
38
- anthropic: 'ANTHROPIC_API_KEY',
39
- openai: 'OPENAI_API_KEY',
40
- 'openai-codex': 'OPENAI_API_KEY',
41
- gemini: 'GEMINI_API_KEY',
42
- groq: 'GROQ_API_KEY',
43
- ollama: '', // No key needed for local ollama
44
- };
45
- function detectLLMConfig(api) {
46
- // Get models from config (agents.defaults.models is a dictionary of models)
47
- const models = api.config.agents?.defaults?.models;
48
- if (!models || Object.keys(models).length === 0) {
49
- throw new Error('No models configured in Moltbot. Please configure at least one model in agents.defaults.models');
50
- }
51
- // Try all configured models to find one with an available API key
52
- const configuredModels = Object.keys(models);
53
- for (const modelKey of configuredModels) {
54
- const [moltbotProvider, ...modelParts] = modelKey.split('/');
55
- const model = modelParts.join('/');
56
- const hindsightProvider = PROVIDER_MAP[moltbotProvider];
57
- if (!hindsightProvider) {
58
- continue; // Skip unsupported providers
27
+ // Provider detection from standard env vars
28
+ const PROVIDER_DETECTION = [
29
+ { name: 'openai', keyEnv: 'OPENAI_API_KEY', defaultModel: 'gpt-4o-mini' },
30
+ { name: 'anthropic', keyEnv: 'ANTHROPIC_API_KEY', defaultModel: 'claude-3-5-haiku-20241022' },
31
+ { name: 'gemini', keyEnv: 'GEMINI_API_KEY', defaultModel: 'gemini-2.5-flash' },
32
+ { name: 'groq', keyEnv: 'GROQ_API_KEY', defaultModel: 'openai/gpt-oss-20b' },
33
+ { name: 'ollama', keyEnv: '', defaultModel: 'llama3.2' },
34
+ ];
35
+ function detectLLMConfig() {
36
+ // Override values from HINDSIGHT_API_LLM_* env vars (highest priority)
37
+ const overrideProvider = process.env.HINDSIGHT_API_LLM_PROVIDER;
38
+ const overrideModel = process.env.HINDSIGHT_API_LLM_MODEL;
39
+ const overrideKey = process.env.HINDSIGHT_API_LLM_API_KEY;
40
+ const overrideBaseUrl = process.env.HINDSIGHT_API_LLM_BASE_URL;
41
+ // If provider is explicitly set, use that (with overrides)
42
+ if (overrideProvider) {
43
+ if (!overrideKey && overrideProvider !== 'ollama') {
44
+ throw new Error(`HINDSIGHT_API_LLM_PROVIDER is set to "${overrideProvider}" but HINDSIGHT_API_LLM_API_KEY is not set.\n` +
45
+ `Please set: export HINDSIGHT_API_LLM_API_KEY=your-api-key`);
59
46
  }
60
- const envKey = ENV_KEY_MAP[moltbotProvider];
61
- const apiKey = envKey ? process.env[envKey] || '' : '';
62
- // For ollama, no key is needed
63
- if (hindsightProvider === 'ollama') {
64
- return { provider: hindsightProvider, apiKey: '', model, envKey: '' };
47
+ const providerInfo = PROVIDER_DETECTION.find(p => p.name === overrideProvider);
48
+ return {
49
+ provider: overrideProvider,
50
+ apiKey: overrideKey || '',
51
+ model: overrideModel || (providerInfo?.defaultModel),
52
+ baseUrl: overrideBaseUrl,
53
+ source: 'HINDSIGHT_API_LLM_PROVIDER override',
54
+ };
55
+ }
56
+ // Auto-detect from standard provider env vars
57
+ for (const providerInfo of PROVIDER_DETECTION) {
58
+ const apiKey = providerInfo.keyEnv ? process.env[providerInfo.keyEnv] : '';
59
+ // Skip ollama in auto-detection (must be explicitly requested)
60
+ if (providerInfo.name === 'ollama') {
61
+ continue;
65
62
  }
66
- // If we found a key, use this provider
67
63
  if (apiKey) {
68
- return { provider: hindsightProvider, apiKey, model, envKey };
64
+ return {
65
+ provider: providerInfo.name,
66
+ apiKey,
67
+ model: overrideModel || providerInfo.defaultModel,
68
+ baseUrl: overrideBaseUrl, // Only use explicit HINDSIGHT_API_LLM_BASE_URL
69
+ source: `auto-detected from ${providerInfo.keyEnv}`,
70
+ };
69
71
  }
70
72
  }
71
- // No API keys found for any provider - show helpful error
72
- const configuredProviders = configuredModels
73
- .map(m => m.split('/')[0])
74
- .filter(p => PROVIDER_MAP[p]);
75
- const keyInstructions = configuredProviders
76
- .map(p => {
77
- const envVar = ENV_KEY_MAP[p];
78
- return envVar ? ` • ${envVar} (for ${p})` : null;
79
- })
80
- .filter(Boolean)
81
- .join('\n');
82
- throw new Error(`No API keys found for Hindsight memory plugin.\n\n` +
83
- `Configured providers in Moltbot: ${configuredProviders.join(', ')}\n\n` +
84
- `Please set one of these environment variables:\n${keyInstructions}\n\n` +
85
- `You can set them in your shell profile (~/.zshrc or ~/.bashrc):\n` +
86
- ` export ANTHROPIC_API_KEY="your-key-here"\n\n` +
87
- `Or run OpenClaw with the environment variable:\n` +
88
- ` ANTHROPIC_API_KEY="your-key" openclaw gateway\n\n` +
89
- `Alternatively, configure ollama provider which doesn't require an API key.`);
73
+ // No configuration found - show helpful error
74
+ throw new Error(`No LLM configuration found for Hindsight memory plugin.\n\n` +
75
+ `Option 1: Set a standard provider API key (auto-detect):\n` +
76
+ ` export OPENAI_API_KEY=sk-your-key # Uses gpt-4o-mini\n` +
77
+ ` export ANTHROPIC_API_KEY=your-key # Uses claude-3-5-haiku\n` +
78
+ ` export GEMINI_API_KEY=your-key # Uses gemini-2.0-flash-exp\n` +
79
+ ` export GROQ_API_KEY=your-key # Uses llama-3.3-70b-versatile\n\n` +
80
+ `Option 2: Override with Hindsight-specific config:\n` +
81
+ ` export HINDSIGHT_API_LLM_PROVIDER=openai\n` +
82
+ ` export HINDSIGHT_API_LLM_MODEL=gpt-4o-mini\n` +
83
+ ` export HINDSIGHT_API_LLM_API_KEY=sk-your-key\n` +
84
+ ` export HINDSIGHT_API_LLM_BASE_URL=https://openrouter.ai/api/v1 # Optional\n\n` +
85
+ `Tip: Use a cheap/fast model for memory extraction (e.g., gpt-4o-mini, claude-3-5-haiku, or free models on OpenRouter)`);
90
86
  }
91
87
  function getPluginConfig(api) {
92
88
  const config = api.config.plugins?.entries?.['hindsight-openclaw']?.config || {};
@@ -101,14 +97,16 @@ function getPluginConfig(api) {
101
97
  export default function (api) {
102
98
  try {
103
99
  console.log('[Hindsight] Plugin loading...');
104
- // Detect LLM configuration from Moltbot
100
+ // Detect LLM configuration from environment
105
101
  console.log('[Hindsight] Detecting LLM config...');
106
- const llmConfig = detectLLMConfig(api);
102
+ const llmConfig = detectLLMConfig();
103
+ const baseUrlInfo = llmConfig.baseUrl ? `, base URL: ${llmConfig.baseUrl}` : '';
104
+ const modelInfo = llmConfig.model || 'default';
107
105
  if (llmConfig.provider === 'ollama') {
108
- console.log(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${llmConfig.model || 'default'} (no API key required)`);
106
+ console.log(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${modelInfo} (${llmConfig.source})`);
109
107
  }
110
108
  else {
111
- console.log(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${llmConfig.model || 'default'} (API key: ${llmConfig.envKey})`);
109
+ console.log(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${modelInfo} (${llmConfig.source}${baseUrlInfo})`);
112
110
  }
113
111
  console.log('[Hindsight] Getting plugin config...');
114
112
  const pluginConfig = getPluginConfig(api);
@@ -125,7 +123,7 @@ export default function (api) {
125
123
  try {
126
124
  // Initialize embed manager
127
125
  console.log('[Hindsight] Creating HindsightEmbedManager...');
128
- embedManager = new HindsightEmbedManager(port, llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.daemonIdleTimeout, pluginConfig.embedVersion);
126
+ embedManager = new HindsightEmbedManager(port, llmConfig.provider, llmConfig.apiKey, llmConfig.model, llmConfig.baseUrl, pluginConfig.daemonIdleTimeout, pluginConfig.embedVersion);
129
127
  // Start the embedded server
130
128
  console.log('[Hindsight] Starting embedded server...');
131
129
  await embedManager.start();
@@ -154,10 +152,46 @@ export default function (api) {
154
152
  api.registerService({
155
153
  id: 'hindsight-memory',
156
154
  async start() {
155
+ console.log('[Hindsight] Service start called - checking daemon health...');
157
156
  // Wait for background init if still pending
158
- console.log('[Hindsight] Service start called - ensuring initialization complete...');
159
- if (initPromise)
160
- await initPromise;
157
+ if (initPromise) {
158
+ try {
159
+ await initPromise;
160
+ }
161
+ catch (error) {
162
+ console.error('[Hindsight] Initial initialization failed:', error);
163
+ // Continue to health check below
164
+ }
165
+ }
166
+ // Check if daemon is actually healthy (handles SIGUSR1 restart case)
167
+ if (embedManager && isInitialized) {
168
+ const healthy = await embedManager.checkHealth();
169
+ if (healthy) {
170
+ console.log('[Hindsight] Daemon is healthy');
171
+ return;
172
+ }
173
+ console.log('[Hindsight] Daemon is not responding - reinitializing...');
174
+ // Reset state for reinitialization
175
+ embedManager = null;
176
+ client = null;
177
+ isInitialized = false;
178
+ }
179
+ // Reinitialize if needed (fresh start or recovery from dead daemon)
180
+ if (!isInitialized) {
181
+ console.log('[Hindsight] Reinitializing daemon...');
182
+ const llmConfig = detectLLMConfig();
183
+ const pluginConfig = getPluginConfig(api);
184
+ const port = pluginConfig.embedPort || Math.floor(Math.random() * 10000) + 10000;
185
+ embedManager = new HindsightEmbedManager(port, llmConfig.provider, llmConfig.apiKey, llmConfig.model, llmConfig.baseUrl, pluginConfig.daemonIdleTimeout, pluginConfig.embedVersion);
186
+ await embedManager.start();
187
+ client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion);
188
+ client.setBankId(BANK_NAME);
189
+ if (pluginConfig.bankMission) {
190
+ await client.setBankMission(pluginConfig.bankMission);
191
+ }
192
+ isInitialized = true;
193
+ console.log('[Hindsight] Reinitialization complete');
194
+ }
161
195
  },
162
196
  async stop() {
163
197
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectorize-io/hindsight-openclaw",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Hindsight memory plugin for OpenClaw - biomimetic long-term memory with fact extraction",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",