@vectorize-io/hindsight-openclaw 0.4.5 → 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.
package/README.md CHANGED
@@ -7,13 +7,13 @@ Biomimetic long-term memory for [OpenClaw](https://openclaw.ai) using [Hindsight
7
7
  ```bash
8
8
  # 1. Configure your LLM provider
9
9
  export OPENAI_API_KEY="sk-your-key"
10
- clawdbot config set 'agents.defaults.models."openai/gpt-4o-mini"' '{}'
10
+ openclaw config set 'agents.defaults.models."openai/gpt-4o-mini"' '{}'
11
11
 
12
12
  # 2. Install and enable the plugin
13
- clawdbot plugins install @vectorize-io/hindsight-openclaw
13
+ openclaw plugins install @vectorize-io/hindsight-openclaw
14
14
 
15
15
  # 3. Start OpenClaw
16
- clawdbot gateway
16
+ openclaw gateway
17
17
  ```
18
18
 
19
19
  That's it! The plugin will automatically start capturing and recalling memories.
@@ -6,13 +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>;
20
+ private writeConfigEnv;
18
21
  }
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'child_process';
2
+ import { promises as fs } from 'fs';
2
3
  import { join } from 'path';
3
4
  import { homedir } from 'os';
4
5
  export class HindsightEmbedManager {
@@ -9,17 +10,19 @@ export class HindsightEmbedManager {
9
10
  llmProvider;
10
11
  llmApiKey;
11
12
  llmModel;
13
+ llmBaseUrl;
12
14
  daemonIdleTimeout;
13
15
  embedVersion;
14
- constructor(port, llmProvider, llmApiKey, llmModel, daemonIdleTimeout = 0, // Default: never timeout
16
+ constructor(port, llmProvider, llmApiKey, llmModel, llmBaseUrl, daemonIdleTimeout = 0, // Default: never timeout
15
17
  embedVersion = 'latest' // Default: latest
16
18
  ) {
17
- this.port = 8889; // hindsight-embed uses fixed port 8889
18
- this.baseUrl = `http://127.0.0.1:8889`;
19
- this.embedDir = join(homedir(), '.clawdbot', 'hindsight-embed');
19
+ this.port = 8888; // hindsight-embed daemon uses same port as API
20
+ this.baseUrl = `http://127.0.0.1:8888`;
21
+ this.embedDir = join(homedir(), '.openclaw', 'hindsight-embed');
20
22
  this.llmProvider = llmProvider;
21
23
  this.llmApiKey = llmApiKey;
22
24
  this.llmModel = llmModel;
25
+ this.llmBaseUrl = llmBaseUrl;
23
26
  this.daemonIdleTimeout = daemonIdleTimeout;
24
27
  this.embedVersion = embedVersion || 'latest';
25
28
  }
@@ -35,6 +38,17 @@ export class HindsightEmbedManager {
35
38
  if (this.llmModel) {
36
39
  env['HINDSIGHT_EMBED_LLM_MODEL'] = this.llmModel;
37
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
+ }
50
+ // Write env vars to ~/.hindsight/config.env for daemon persistence
51
+ await this.writeConfigEnv(env);
38
52
  // Start hindsight-embed daemon (it manages itself)
39
53
  const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
40
54
  const startDaemon = spawn('uvx', [embedPackage, 'daemon', 'start'], {
@@ -117,4 +131,78 @@ export class HindsightEmbedManager {
117
131
  isRunning() {
118
132
  return this.process !== null;
119
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
+ }
143
+ async writeConfigEnv(env) {
144
+ const hindsightDir = join(homedir(), '.hindsight');
145
+ const embedConfigPath = join(hindsightDir, 'embed');
146
+ // Ensure directory exists
147
+ await fs.mkdir(hindsightDir, { recursive: true });
148
+ // Read existing config to preserve extra settings
149
+ let existingContent = '';
150
+ let extraSettings = [];
151
+ try {
152
+ existingContent = await fs.readFile(embedConfigPath, 'utf-8');
153
+ // Extract non-LLM settings (like FORCE_CPU flags)
154
+ const lines = existingContent.split('\n');
155
+ for (const line of lines) {
156
+ const trimmed = line.trim();
157
+ if (trimmed && !trimmed.startsWith('#') &&
158
+ !trimmed.startsWith('HINDSIGHT_EMBED_LLM_') &&
159
+ !trimmed.startsWith('HINDSIGHT_API_LLM_') &&
160
+ !trimmed.startsWith('HINDSIGHT_EMBED_BANK_ID') &&
161
+ !trimmed.startsWith('HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT')) {
162
+ extraSettings.push(line);
163
+ }
164
+ }
165
+ }
166
+ catch {
167
+ // File doesn't exist yet, that's fine
168
+ }
169
+ // Build config file with header
170
+ const configLines = [
171
+ '# Hindsight Embed Configuration',
172
+ '# Generated by OpenClaw Hindsight plugin',
173
+ '',
174
+ ];
175
+ // Add LLM config
176
+ if (env.HINDSIGHT_EMBED_LLM_PROVIDER) {
177
+ configLines.push(`HINDSIGHT_EMBED_LLM_PROVIDER=${env.HINDSIGHT_EMBED_LLM_PROVIDER}`);
178
+ }
179
+ if (env.HINDSIGHT_EMBED_LLM_MODEL) {
180
+ configLines.push(`HINDSIGHT_EMBED_LLM_MODEL=${env.HINDSIGHT_EMBED_LLM_MODEL}`);
181
+ }
182
+ if (env.HINDSIGHT_EMBED_LLM_API_KEY) {
183
+ configLines.push(`HINDSIGHT_EMBED_LLM_API_KEY=${env.HINDSIGHT_EMBED_LLM_API_KEY}`);
184
+ }
185
+ if (env.HINDSIGHT_API_LLM_BASE_URL) {
186
+ configLines.push(`HINDSIGHT_API_LLM_BASE_URL=${env.HINDSIGHT_API_LLM_BASE_URL}`);
187
+ }
188
+ if (env.HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT) {
189
+ configLines.push(`HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT=${env.HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT}`);
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
+ }
198
+ // Add extra settings if they exist
199
+ if (extraSettings.length > 0) {
200
+ configLines.push('');
201
+ configLines.push('# Additional settings');
202
+ configLines.push(...extraSettings);
203
+ }
204
+ // Write to file
205
+ await fs.writeFile(embedConfigPath, configLines.join('\n') + '\n', 'utf-8');
206
+ console.log(`[Hindsight] Wrote config to ${embedConfigPath}`);
207
+ }
120
208
  }
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 Moltbot with the environment variable:\n` +
88
- ` ANTHROPIC_API_KEY="your-key" clawdbot start\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 {
@@ -215,10 +249,10 @@ export default function (api) {
215
249
  return;
216
250
  }
217
251
  console.log('[Hindsight] Auto-recall for prompt:', prompt.substring(0, 50));
218
- // Recall relevant memories (up to 1024 tokens)
252
+ // Recall relevant memories (up to 512 tokens)
219
253
  const response = await client.recall({
220
254
  query: prompt,
221
- max_tokens: 1024,
255
+ max_tokens: 512,
222
256
  });
223
257
  if (!response.results || response.results.length === 0) {
224
258
  console.log('[Hindsight] No memories found for auto-recall');
@@ -227,7 +261,10 @@ export default function (api) {
227
261
  // Format memories as JSON with all fields from recall
228
262
  const memoriesJson = JSON.stringify(response.results, null, 2);
229
263
  const contextMessage = `<hindsight_memories>
264
+ Relevant memories from past conversations (score 1=highest, prioritize recent when conflicting):
230
265
  ${memoriesJson}
266
+
267
+ User message: ${prompt}
231
268
  </hindsight_memories>`;
232
269
  console.log(`[Hindsight] Auto-recall: Injecting ${response.results.length} memories`);
233
270
  // Inject context before the user message
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@vectorize-io/hindsight-openclaw",
3
- "version": "0.4.5",
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",
7
7
  "type": "module",
8
- "clawdbot": {
8
+ "openclaw": {
9
9
  "extensions": [
10
10
  "./dist/index.js"
11
11
  ]
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "files": [
29
29
  "dist",
30
- "clawdbot.plugin.json",
30
+ "openclaw.plugin.json",
31
31
  "README.md"
32
32
  ],
33
33
  "scripts": {
File without changes