@vectorize-io/hindsight-openclaw 0.4.7 → 0.4.9

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
@@ -5,9 +5,15 @@ Biomimetic long-term memory for [OpenClaw](https://openclaw.ai) using [Hindsight
5
5
  ## Quick Start
6
6
 
7
7
  ```bash
8
- # 1. Configure your LLM provider
8
+ # 1. Configure your LLM provider for memory extraction
9
+ # Option A: OpenAI
9
10
  export OPENAI_API_KEY="sk-your-key"
10
- openclaw config set 'agents.defaults.models."openai/gpt-4o-mini"' '{}'
11
+
12
+ # Option B: Claude Code (no API key needed)
13
+ export HINDSIGHT_API_LLM_PROVIDER=claude-code
14
+
15
+ # Option C: OpenAI Codex (no API key needed)
16
+ export HINDSIGHT_API_LLM_PROVIDER=openai-codex
11
17
 
12
18
  # 2. Install and enable the plugin
13
19
  openclaw plugins install @vectorize-io/hindsight-openclaw
@@ -24,6 +30,40 @@ For full documentation, configuration options, troubleshooting, and development
24
30
 
25
31
  **[OpenClaw Integration Documentation](https://vectorize.io/hindsight/sdks/integrations/openclaw)**
26
32
 
33
+ ## Development
34
+
35
+ To test local changes to the Hindsight package before publishing:
36
+
37
+ 1. Add `embedPackagePath` to your plugin config in `~/.openclaw/openclaw.json`:
38
+ ```json
39
+ {
40
+ "plugins": {
41
+ "entries": {
42
+ "hindsight-openclaw": {
43
+ "enabled": true,
44
+ "config": {
45
+ "embedPackagePath": "/path/to/hindsight-wt3/hindsight-embed"
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ 2. The plugin will use `uv run --directory <path> hindsight-embed` instead of `uvx hindsight-embed@latest`
54
+
55
+ 3. To use a specific profile for testing:
56
+ ```bash
57
+ # Check daemon status
58
+ uvx hindsight-embed@latest -p openclaw daemon status
59
+
60
+ # View logs
61
+ tail -f ~/.hindsight/profiles/openclaw.log
62
+
63
+ # List profiles
64
+ uvx hindsight-embed@latest profile list
65
+ ```
66
+
27
67
  ## Links
28
68
 
29
69
  - [Hindsight Documentation](https://vectorize.io/hindsight)
package/dist/client.d.ts CHANGED
@@ -1,14 +1,41 @@
1
1
  import type { RetainRequest, RetainResponse, RecallRequest, RecallResponse } from './types.js';
2
+ /**
3
+ * Escape a string for use as a single-quoted shell argument.
4
+ *
5
+ * In POSIX shells, single-quoted strings treat ALL characters literally
6
+ * except for the single quote itself. To include a literal single quote,
7
+ * we use the pattern: end quote + escaped quote + start quote = '\''
8
+ *
9
+ * Example: "It's $100" becomes 'It'\''s $100'
10
+ * Shell interprets: 'It' + \' + 's $100' = It's $100
11
+ *
12
+ * This handles ALL shell-special characters including:
13
+ * - $ (variable expansion)
14
+ * - ` (command substitution)
15
+ * - ! (history expansion)
16
+ * - ? * [ ] (glob patterns)
17
+ * - ( ) { } (subshell/brace expansion)
18
+ * - < > | & ; (redirection/control)
19
+ * - \ " # ~ newlines
20
+ *
21
+ * @param arg - The string to escape
22
+ * @returns The escaped string (without surrounding quotes - caller adds those)
23
+ */
24
+ export declare function escapeShellArg(arg: string): string;
2
25
  export declare class HindsightClient {
3
26
  private bankId;
4
27
  private llmProvider;
5
28
  private llmApiKey;
6
29
  private llmModel?;
7
30
  private embedVersion;
8
- constructor(llmProvider: string, llmApiKey: string, llmModel?: string, embedVersion?: string);
31
+ private embedPackagePath?;
32
+ constructor(llmProvider: string, llmApiKey: string, llmModel?: string, embedVersion?: string, embedPackagePath?: string);
33
+ /**
34
+ * Get the command prefix to run hindsight-embed (either local or from PyPI)
35
+ */
36
+ private getEmbedCommandPrefix;
9
37
  setBankId(bankId: string): void;
10
38
  setBankMission(mission: string): Promise<void>;
11
- private getEnv;
12
39
  retain(request: RetainRequest): Promise<RetainResponse>;
13
40
  recall(request: RecallRequest): Promise<RecallResponse>;
14
41
  }
package/dist/client.js CHANGED
@@ -1,17 +1,61 @@
1
1
  import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  const execAsync = promisify(exec);
4
+ /**
5
+ * Escape a string for use as a single-quoted shell argument.
6
+ *
7
+ * In POSIX shells, single-quoted strings treat ALL characters literally
8
+ * except for the single quote itself. To include a literal single quote,
9
+ * we use the pattern: end quote + escaped quote + start quote = '\''
10
+ *
11
+ * Example: "It's $100" becomes 'It'\''s $100'
12
+ * Shell interprets: 'It' + \' + 's $100' = It's $100
13
+ *
14
+ * This handles ALL shell-special characters including:
15
+ * - $ (variable expansion)
16
+ * - ` (command substitution)
17
+ * - ! (history expansion)
18
+ * - ? * [ ] (glob patterns)
19
+ * - ( ) { } (subshell/brace expansion)
20
+ * - < > | & ; (redirection/control)
21
+ * - \ " # ~ newlines
22
+ *
23
+ * @param arg - The string to escape
24
+ * @returns The escaped string (without surrounding quotes - caller adds those)
25
+ */
26
+ export function escapeShellArg(arg) {
27
+ // Replace single quotes with the escape sequence: '\''
28
+ // This ends the current single-quoted string, adds an escaped literal quote,
29
+ // and starts a new single-quoted string.
30
+ return arg.replace(/'/g, "'\\''");
31
+ }
4
32
  export class HindsightClient {
5
33
  bankId = 'default'; // Always use default bank
6
34
  llmProvider;
7
35
  llmApiKey;
8
36
  llmModel;
9
37
  embedVersion;
10
- constructor(llmProvider, llmApiKey, llmModel, embedVersion = 'latest') {
38
+ embedPackagePath;
39
+ constructor(llmProvider, llmApiKey, llmModel, embedVersion = 'latest', embedPackagePath) {
11
40
  this.llmProvider = llmProvider;
12
41
  this.llmApiKey = llmApiKey;
13
42
  this.llmModel = llmModel;
14
43
  this.embedVersion = embedVersion || 'latest';
44
+ this.embedPackagePath = embedPackagePath;
45
+ }
46
+ /**
47
+ * Get the command prefix to run hindsight-embed (either local or from PyPI)
48
+ */
49
+ getEmbedCommandPrefix() {
50
+ if (this.embedPackagePath) {
51
+ // Local package: uv run --directory <path> hindsight-embed
52
+ return `uv run --directory ${this.embedPackagePath} hindsight-embed`;
53
+ }
54
+ else {
55
+ // PyPI package: uvx hindsight-embed@version
56
+ const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
57
+ return `uvx ${embedPackage}`;
58
+ }
15
59
  }
16
60
  setBankId(bankId) {
17
61
  this.bankId = bankId;
@@ -20,11 +64,11 @@ export class HindsightClient {
20
64
  if (!mission || mission.trim().length === 0) {
21
65
  return;
22
66
  }
23
- const escapedMission = mission.replace(/'/g, "'\\''"); // Escape single quotes
24
- const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
25
- const cmd = `uvx ${embedPackage} bank mission ${this.bankId} '${escapedMission}'`;
67
+ const escapedMission = escapeShellArg(mission);
68
+ const embedCmd = this.getEmbedCommandPrefix();
69
+ const cmd = `${embedCmd} --profile openclaw bank mission ${this.bankId} '${escapedMission}'`;
26
70
  try {
27
- const { stdout } = await execAsync(cmd, { env: this.getEnv() });
71
+ const { stdout } = await execAsync(cmd);
28
72
  console.log(`[Hindsight] Bank mission set: ${stdout.trim()}`);
29
73
  }
30
74
  catch (error) {
@@ -32,24 +76,13 @@ export class HindsightClient {
32
76
  console.warn(`[Hindsight] Could not set bank mission (bank may not exist yet): ${error}`);
33
77
  }
34
78
  }
35
- getEnv() {
36
- const env = {
37
- ...process.env,
38
- HINDSIGHT_EMBED_LLM_PROVIDER: this.llmProvider,
39
- HINDSIGHT_EMBED_LLM_API_KEY: this.llmApiKey,
40
- };
41
- if (this.llmModel) {
42
- env.HINDSIGHT_EMBED_LLM_MODEL = this.llmModel;
43
- }
44
- return env;
45
- }
46
79
  async retain(request) {
47
- const content = request.content.replace(/'/g, "'\\''"); // Escape single quotes
48
- const docId = request.document_id || 'conversation';
49
- const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
50
- const cmd = `uvx ${embedPackage} memory retain ${this.bankId} '${content}' --doc-id '${docId}' --async`;
80
+ const content = escapeShellArg(request.content);
81
+ const docId = escapeShellArg(request.document_id || 'conversation');
82
+ const embedCmd = this.getEmbedCommandPrefix();
83
+ const cmd = `${embedCmd} --profile openclaw memory retain ${this.bankId} '${content}' --doc-id '${docId}' --async`;
51
84
  try {
52
- const { stdout } = await execAsync(cmd, { env: this.getEnv() });
85
+ const { stdout } = await execAsync(cmd);
53
86
  console.log(`[Hindsight] Retained (async): ${stdout.trim()}`);
54
87
  // Return a simple response
55
88
  return {
@@ -63,12 +96,12 @@ export class HindsightClient {
63
96
  }
64
97
  }
65
98
  async recall(request) {
66
- const query = request.query.replace(/'/g, "'\\''"); // Escape single quotes
99
+ const query = escapeShellArg(request.query);
67
100
  const maxTokens = request.max_tokens || 1024;
68
- const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
69
- const cmd = `uvx ${embedPackage} memory recall ${this.bankId} '${query}' --output json --max-tokens ${maxTokens}`;
101
+ const embedCmd = this.getEmbedCommandPrefix();
102
+ const cmd = `${embedCmd} --profile openclaw memory recall ${this.bankId} '${query}' --output json --max-tokens ${maxTokens}`;
70
103
  try {
71
- const { stdout } = await execAsync(cmd, { env: this.getEnv() });
104
+ const { stdout } = await execAsync(cmd);
72
105
  // Parse JSON output - returns { entities: {...}, results: [...] }
73
106
  const response = JSON.parse(stdout);
74
107
  const results = response.results || [];
@@ -9,13 +9,19 @@ export declare class HindsightEmbedManager {
9
9
  private llmBaseUrl?;
10
10
  private daemonIdleTimeout;
11
11
  private embedVersion;
12
+ private embedPackagePath?;
12
13
  constructor(port: number, llmProvider: string, llmApiKey: string, llmModel?: string, llmBaseUrl?: string, daemonIdleTimeout?: number, // Default: never timeout
13
- embedVersion?: string);
14
+ embedVersion?: string, // Default: latest
15
+ embedPackagePath?: string);
16
+ /**
17
+ * Get the command to run hindsight-embed (either local or from PyPI)
18
+ */
19
+ private getEmbedCommand;
14
20
  start(): Promise<void>;
15
21
  stop(): Promise<void>;
16
22
  private waitForReady;
17
23
  getBaseUrl(): string;
18
24
  isRunning(): boolean;
19
25
  checkHealth(): Promise<boolean>;
20
- private writeConfigEnv;
26
+ private configureProfile;
21
27
  }
@@ -1,5 +1,4 @@
1
1
  import { spawn } from 'child_process';
2
- import { promises as fs } from 'fs';
3
2
  import { join } from 'path';
4
3
  import { homedir } from 'os';
5
4
  export class HindsightEmbedManager {
@@ -13,11 +12,14 @@ export class HindsightEmbedManager {
13
12
  llmBaseUrl;
14
13
  daemonIdleTimeout;
15
14
  embedVersion;
15
+ embedPackagePath;
16
16
  constructor(port, llmProvider, llmApiKey, llmModel, llmBaseUrl, daemonIdleTimeout = 0, // Default: never timeout
17
- embedVersion = 'latest' // Default: latest
17
+ embedVersion = 'latest', // Default: latest
18
+ embedPackagePath // Local path to hindsight package
18
19
  ) {
19
- this.port = 8888; // hindsight-embed daemon uses same port as API
20
- this.baseUrl = `http://127.0.0.1:8888`;
20
+ // Use the configured port (default: 9077 from config)
21
+ this.port = port;
22
+ this.baseUrl = `http://127.0.0.1:${port}`;
21
23
  this.embedDir = join(homedir(), '.openclaw', 'hindsight-embed');
22
24
  this.llmProvider = llmProvider;
23
25
  this.llmApiKey = llmApiKey;
@@ -25,18 +27,33 @@ export class HindsightEmbedManager {
25
27
  this.llmBaseUrl = llmBaseUrl;
26
28
  this.daemonIdleTimeout = daemonIdleTimeout;
27
29
  this.embedVersion = embedVersion || 'latest';
30
+ this.embedPackagePath = embedPackagePath;
31
+ }
32
+ /**
33
+ * Get the command to run hindsight-embed (either local or from PyPI)
34
+ */
35
+ getEmbedCommand() {
36
+ if (this.embedPackagePath) {
37
+ // Local package: uv run --directory <path> hindsight-embed
38
+ return ['uv', 'run', '--directory', this.embedPackagePath, 'hindsight-embed'];
39
+ }
40
+ else {
41
+ // PyPI package: uvx hindsight-embed@version
42
+ const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
43
+ return ['uvx', embedPackage];
44
+ }
28
45
  }
29
46
  async start() {
30
47
  console.log(`[Hindsight] Starting hindsight-embed daemon...`);
31
- // Build environment variables
48
+ // Build environment variables using standard HINDSIGHT_API_LLM_* variables
32
49
  const env = {
33
50
  ...process.env,
34
- HINDSIGHT_EMBED_LLM_PROVIDER: this.llmProvider,
35
- HINDSIGHT_EMBED_LLM_API_KEY: this.llmApiKey,
51
+ HINDSIGHT_API_LLM_PROVIDER: this.llmProvider,
52
+ HINDSIGHT_API_LLM_API_KEY: this.llmApiKey,
36
53
  HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT: this.daemonIdleTimeout.toString(),
37
54
  };
38
55
  if (this.llmModel) {
39
- env['HINDSIGHT_EMBED_LLM_MODEL'] = this.llmModel;
56
+ env['HINDSIGHT_API_LLM_MODEL'] = this.llmModel;
40
57
  }
41
58
  // Pass through base URL for OpenAI-compatible providers (OpenRouter, etc.)
42
59
  if (this.llmBaseUrl) {
@@ -47,12 +64,12 @@ export class HindsightEmbedManager {
47
64
  env['HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU'] = '1';
48
65
  env['HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU'] = '1';
49
66
  }
50
- // Write env vars to ~/.hindsight/config.env for daemon persistence
51
- await this.writeConfigEnv(env);
52
- // Start hindsight-embed daemon (it manages itself)
53
- const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
54
- const startDaemon = spawn('uvx', [embedPackage, 'daemon', 'start'], {
55
- env,
67
+ // Configure "openclaw" profile using hindsight-embed configure (non-interactive)
68
+ console.log('[Hindsight] Configuring "openclaw" profile...');
69
+ await this.configureProfile(env);
70
+ // Start hindsight-embed daemon with openclaw profile
71
+ const embedCmd = this.getEmbedCommand();
72
+ const startDaemon = spawn(embedCmd[0], [...embedCmd.slice(1), 'daemon', '--profile', 'openclaw', 'start'], {
56
73
  stdio: 'pipe',
57
74
  });
58
75
  // Collect output
@@ -88,8 +105,8 @@ export class HindsightEmbedManager {
88
105
  }
89
106
  async stop() {
90
107
  console.log('[Hindsight] Stopping hindsight-embed daemon...');
91
- const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
92
- const stopDaemon = spawn('uvx', [embedPackage, 'daemon', 'stop'], {
108
+ const embedCmd = this.getEmbedCommand();
109
+ const stopDaemon = spawn(embedCmd[0], [...embedCmd.slice(1), 'daemon', '--profile', 'openclaw', 'stop'], {
93
110
  stdio: 'pipe',
94
111
  });
95
112
  await new Promise((resolve) => {
@@ -140,69 +157,54 @@ export class HindsightEmbedManager {
140
157
  return false;
141
158
  }
142
159
  }
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
- '',
160
+ async configureProfile(env) {
161
+ // Build profile create command args with --merge, --port and --env flags
162
+ // Use --merge to allow updating existing profile
163
+ const createArgs = ['profile', 'create', 'openclaw', '--merge', '--port', this.port.toString()];
164
+ // Add all environment variables as --env flags
165
+ const envVars = [
166
+ 'HINDSIGHT_API_LLM_PROVIDER',
167
+ 'HINDSIGHT_API_LLM_MODEL',
168
+ 'HINDSIGHT_API_LLM_API_KEY',
169
+ 'HINDSIGHT_API_LLM_BASE_URL',
170
+ 'HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT',
171
+ 'HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU',
172
+ 'HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU',
174
173
  ];
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);
174
+ for (const envVar of envVars) {
175
+ if (env[envVar]) {
176
+ createArgs.push('--env', `${envVar}=${env[envVar]}`);
177
+ }
203
178
  }
204
- // Write to file
205
- await fs.writeFile(embedConfigPath, configLines.join('\n') + '\n', 'utf-8');
206
- console.log(`[Hindsight] Wrote config to ${embedConfigPath}`);
179
+ // Run profile create command (non-interactive, overwrites if exists)
180
+ const embedCmd = this.getEmbedCommand();
181
+ const create = spawn(embedCmd[0], [...embedCmd.slice(1), ...createArgs], {
182
+ stdio: 'pipe',
183
+ });
184
+ let output = '';
185
+ create.stdout?.on('data', (data) => {
186
+ const text = data.toString();
187
+ output += text;
188
+ console.log(`[Hindsight] ${text.trim()}`);
189
+ });
190
+ create.stderr?.on('data', (data) => {
191
+ const text = data.toString();
192
+ output += text;
193
+ console.error(`[Hindsight] ${text.trim()}`);
194
+ });
195
+ await new Promise((resolve, reject) => {
196
+ create.on('exit', (code) => {
197
+ if (code === 0) {
198
+ console.log('[Hindsight] Profile "openclaw" configured successfully');
199
+ resolve();
200
+ }
201
+ else {
202
+ reject(new Error(`Profile create failed with code ${code}: ${output}`));
203
+ }
204
+ });
205
+ create.on('error', (error) => {
206
+ reject(error);
207
+ });
208
+ });
207
209
  }
208
210
  }
package/dist/index.js CHANGED
@@ -7,6 +7,11 @@ let embedManager = null;
7
7
  let client = null;
8
8
  let initPromise = null;
9
9
  let isInitialized = false;
10
+ let usingExternalApi = false; // Track if using external API (skip daemon management)
11
+ // Store the current plugin config for bank ID derivation
12
+ let currentPluginConfig = null;
13
+ // Track which banks have had their mission set (to avoid re-setting on every request)
14
+ const banksWithMissionSet = new Set();
10
15
  // Global access for hooks (Moltbot loads hooks separately)
11
16
  if (typeof global !== 'undefined') {
12
17
  global.__hindsightClient = {
@@ -17,13 +22,59 @@ if (typeof global !== 'undefined') {
17
22
  if (initPromise)
18
23
  await initPromise;
19
24
  },
25
+ /**
26
+ * Get a client configured for a specific agent context.
27
+ * Derives the bank ID from the context for per-channel isolation.
28
+ * Also ensures the bank mission is set on first use.
29
+ */
30
+ getClientForContext: async (ctx) => {
31
+ if (!client)
32
+ return null;
33
+ const config = currentPluginConfig || {};
34
+ const bankId = deriveBankId(ctx, config);
35
+ client.setBankId(bankId);
36
+ // Set bank mission on first use of this bank (if configured)
37
+ if (config.bankMission && config.dynamicBankId && !banksWithMissionSet.has(bankId)) {
38
+ try {
39
+ await client.setBankMission(config.bankMission);
40
+ banksWithMissionSet.add(bankId);
41
+ console.log(`[Hindsight] Set mission for new bank: ${bankId}`);
42
+ }
43
+ catch (error) {
44
+ // Log but don't fail - bank mission is not critical
45
+ console.warn(`[Hindsight] Could not set bank mission for ${bankId}: ${error}`);
46
+ }
47
+ }
48
+ return client;
49
+ },
50
+ getPluginConfig: () => currentPluginConfig,
20
51
  };
21
52
  }
22
53
  // Get directory of current module
23
54
  const __filename = fileURLToPath(import.meta.url);
24
55
  const __dirname = dirname(__filename);
25
- // Default bank name
26
- const BANK_NAME = 'openclaw';
56
+ // Default bank name (fallback when channel context not available)
57
+ const DEFAULT_BANK_NAME = 'openclaw';
58
+ /**
59
+ * Derive a bank ID from the agent context.
60
+ * Creates channel-specific banks: {messageProvider}-{channelId}
61
+ * Falls back to default bank when context is unavailable.
62
+ */
63
+ function deriveBankId(ctx, pluginConfig) {
64
+ // If dynamic bank ID is disabled, use static bank
65
+ if (pluginConfig.dynamicBankId === false) {
66
+ return pluginConfig.bankIdPrefix
67
+ ? `${pluginConfig.bankIdPrefix}-${DEFAULT_BANK_NAME}`
68
+ : DEFAULT_BANK_NAME;
69
+ }
70
+ const channelType = ctx?.messageProvider || 'unknown';
71
+ const channelId = ctx?.channelId || 'default';
72
+ // Build bank ID: {prefix?}-{channelType}-{channelId}
73
+ const baseBankId = `${channelType}-${channelId}`;
74
+ return pluginConfig.bankIdPrefix
75
+ ? `${pluginConfig.bankIdPrefix}-${baseBankId}`
76
+ : baseBankId;
77
+ }
27
78
  // Provider detection from standard env vars
28
79
  const PROVIDER_DETECTION = [
29
80
  { name: 'openai', keyEnv: 'OPENAI_API_KEY', defaultModel: 'gpt-4o-mini' },
@@ -31,16 +82,20 @@ const PROVIDER_DETECTION = [
31
82
  { name: 'gemini', keyEnv: 'GEMINI_API_KEY', defaultModel: 'gemini-2.5-flash' },
32
83
  { name: 'groq', keyEnv: 'GROQ_API_KEY', defaultModel: 'openai/gpt-oss-20b' },
33
84
  { name: 'ollama', keyEnv: '', defaultModel: 'llama3.2' },
85
+ { name: 'openai-codex', keyEnv: '', defaultModel: 'gpt-5.2-codex' },
86
+ { name: 'claude-code', keyEnv: '', defaultModel: 'claude-sonnet-4-5-20250929' },
34
87
  ];
35
- function detectLLMConfig() {
88
+ function detectLLMConfig(pluginConfig) {
36
89
  // Override values from HINDSIGHT_API_LLM_* env vars (highest priority)
37
90
  const overrideProvider = process.env.HINDSIGHT_API_LLM_PROVIDER;
38
91
  const overrideModel = process.env.HINDSIGHT_API_LLM_MODEL;
39
92
  const overrideKey = process.env.HINDSIGHT_API_LLM_API_KEY;
40
93
  const overrideBaseUrl = process.env.HINDSIGHT_API_LLM_BASE_URL;
41
- // If provider is explicitly set, use that (with overrides)
94
+ // Priority 1: If provider is explicitly set via env var, use that
42
95
  if (overrideProvider) {
43
- if (!overrideKey && overrideProvider !== 'ollama') {
96
+ // Providers that don't require an API key (use OAuth or local models)
97
+ const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
98
+ if (!overrideKey && !noKeyRequired.includes(overrideProvider)) {
44
99
  throw new Error(`HINDSIGHT_API_LLM_PROVIDER is set to "${overrideProvider}" but HINDSIGHT_API_LLM_API_KEY is not set.\n` +
45
100
  `Please set: export HINDSIGHT_API_LLM_API_KEY=your-api-key`);
46
101
  }
@@ -53,11 +108,39 @@ function detectLLMConfig() {
53
108
  source: 'HINDSIGHT_API_LLM_PROVIDER override',
54
109
  };
55
110
  }
56
- // Auto-detect from standard provider env vars
111
+ // Priority 2: Plugin config llmProvider/llmModel
112
+ if (pluginConfig?.llmProvider) {
113
+ const providerInfo = PROVIDER_DETECTION.find(p => p.name === pluginConfig.llmProvider);
114
+ // Resolve API key: llmApiKeyEnv > provider's standard keyEnv
115
+ let apiKey = '';
116
+ if (pluginConfig.llmApiKeyEnv) {
117
+ apiKey = process.env[pluginConfig.llmApiKeyEnv] || '';
118
+ }
119
+ else if (providerInfo?.keyEnv) {
120
+ apiKey = process.env[providerInfo.keyEnv] || '';
121
+ }
122
+ // Providers that don't require an API key (use OAuth or local models)
123
+ const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
124
+ if (!apiKey && !noKeyRequired.includes(pluginConfig.llmProvider)) {
125
+ const keySource = pluginConfig.llmApiKeyEnv || providerInfo?.keyEnv || 'unknown';
126
+ throw new Error(`Plugin config llmProvider is set to "${pluginConfig.llmProvider}" but no API key found.\n` +
127
+ `Expected env var: ${keySource}\n` +
128
+ `Set the env var or use llmApiKeyEnv in plugin config to specify a custom env var name.`);
129
+ }
130
+ return {
131
+ provider: pluginConfig.llmProvider,
132
+ apiKey,
133
+ model: pluginConfig.llmModel || overrideModel || providerInfo?.defaultModel,
134
+ baseUrl: overrideBaseUrl,
135
+ source: 'plugin config',
136
+ };
137
+ }
138
+ // Priority 3: Auto-detect from standard provider env vars
57
139
  for (const providerInfo of PROVIDER_DETECTION) {
58
140
  const apiKey = providerInfo.keyEnv ? process.env[providerInfo.keyEnv] : '';
59
- // Skip ollama in auto-detection (must be explicitly requested)
60
- if (providerInfo.name === 'ollama') {
141
+ // Skip providers that don't use API keys in auto-detection (must be explicitly requested)
142
+ const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
143
+ if (noKeyRequired.includes(providerInfo.name)) {
61
144
  continue;
62
145
  }
63
146
  if (apiKey) {
@@ -75,15 +158,47 @@ function detectLLMConfig() {
75
158
  `Option 1: Set a standard provider API key (auto-detect):\n` +
76
159
  ` export OPENAI_API_KEY=sk-your-key # Uses gpt-4o-mini\n` +
77
160
  ` 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` +
161
+ ` export GEMINI_API_KEY=your-key # Uses gemini-2.5-flash\n` +
162
+ ` export GROQ_API_KEY=your-key # Uses openai/gpt-oss-20b\n\n` +
163
+ `Option 2: Use Codex or Claude Code (no API key needed):\n` +
164
+ ` export HINDSIGHT_API_LLM_PROVIDER=openai-codex # Requires 'codex auth login'\n` +
165
+ ` export HINDSIGHT_API_LLM_PROVIDER=claude-code # Requires Claude Code CLI\n\n` +
166
+ `Option 3: Set llmProvider in openclaw.json plugin config:\n` +
167
+ ` "llmProvider": "openai", "llmModel": "gpt-4o-mini"\n\n` +
168
+ `Option 4: Override with Hindsight-specific env vars:\n` +
81
169
  ` export HINDSIGHT_API_LLM_PROVIDER=openai\n` +
82
170
  ` export HINDSIGHT_API_LLM_MODEL=gpt-4o-mini\n` +
83
171
  ` export HINDSIGHT_API_LLM_API_KEY=sk-your-key\n` +
84
172
  ` export HINDSIGHT_API_LLM_BASE_URL=https://openrouter.ai/api/v1 # Optional\n\n` +
85
173
  `Tip: Use a cheap/fast model for memory extraction (e.g., gpt-4o-mini, claude-3-5-haiku, or free models on OpenRouter)`);
86
174
  }
175
+ /**
176
+ * Detect external Hindsight API configuration.
177
+ * Priority: env vars > plugin config
178
+ */
179
+ function detectExternalApi(pluginConfig) {
180
+ const apiUrl = process.env.HINDSIGHT_EMBED_API_URL || pluginConfig?.hindsightApiUrl || null;
181
+ const apiToken = process.env.HINDSIGHT_EMBED_API_TOKEN || pluginConfig?.hindsightApiToken || null;
182
+ return { apiUrl, apiToken };
183
+ }
184
+ /**
185
+ * Health check for external Hindsight API.
186
+ */
187
+ async function checkExternalApiHealth(apiUrl) {
188
+ const healthUrl = `${apiUrl.replace(/\/$/, '')}/health`;
189
+ console.log(`[Hindsight] Checking external API health at ${healthUrl}...`);
190
+ try {
191
+ const response = await fetch(healthUrl, { signal: AbortSignal.timeout(10000) });
192
+ if (!response.ok) {
193
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
194
+ }
195
+ const data = await response.json();
196
+ console.log(`[Hindsight] External API health: ${JSON.stringify(data)}`);
197
+ }
198
+ catch (error) {
199
+ throw new Error(`Cannot connect to external Hindsight API at ${apiUrl}: ${error}`);
200
+ }
201
+ }
87
202
  function getPluginConfig(api) {
88
203
  const config = api.config.plugins?.entries?.['hindsight-openclaw']?.config || {};
89
204
  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.';
@@ -92,14 +207,29 @@ function getPluginConfig(api) {
92
207
  embedPort: config.embedPort || 0,
93
208
  daemonIdleTimeout: config.daemonIdleTimeout !== undefined ? config.daemonIdleTimeout : 0,
94
209
  embedVersion: config.embedVersion || 'latest',
210
+ embedPackagePath: config.embedPackagePath,
211
+ llmProvider: config.llmProvider,
212
+ llmModel: config.llmModel,
213
+ llmApiKeyEnv: config.llmApiKeyEnv,
214
+ hindsightApiUrl: config.hindsightApiUrl,
215
+ hindsightApiToken: config.hindsightApiToken,
216
+ apiPort: config.apiPort || 9077,
217
+ // Dynamic bank ID options (default: enabled)
218
+ dynamicBankId: config.dynamicBankId !== false,
219
+ bankIdPrefix: config.bankIdPrefix,
95
220
  };
96
221
  }
97
222
  export default function (api) {
98
223
  try {
99
224
  console.log('[Hindsight] Plugin loading...');
100
- // Detect LLM configuration from environment
225
+ // Get plugin config first (needed for LLM detection)
226
+ console.log('[Hindsight] Getting plugin config...');
227
+ const pluginConfig = getPluginConfig(api);
228
+ // Store config globally for bank ID derivation in hooks
229
+ currentPluginConfig = pluginConfig;
230
+ // Detect LLM configuration (env vars > plugin config > auto-detect)
101
231
  console.log('[Hindsight] Detecting LLM config...');
102
- const llmConfig = detectLLMConfig();
232
+ const llmConfig = detectLLMConfig(pluginConfig);
103
233
  const baseUrlInfo = llmConfig.baseUrl ? `, base URL: ${llmConfig.baseUrl}` : '';
104
234
  const modelInfo = llmConfig.model || 'default';
105
235
  if (llmConfig.provider === 'ollama') {
@@ -108,38 +238,83 @@ export default function (api) {
108
238
  else {
109
239
  console.log(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${modelInfo} (${llmConfig.source}${baseUrlInfo})`);
110
240
  }
111
- console.log('[Hindsight] Getting plugin config...');
112
- const pluginConfig = getPluginConfig(api);
113
241
  if (pluginConfig.bankMission) {
114
242
  console.log(`[Hindsight] Custom bank mission configured: "${pluginConfig.bankMission.substring(0, 50)}..."`);
115
243
  }
116
- console.log(`[Hindsight] Daemon idle timeout: ${pluginConfig.daemonIdleTimeout}s (0 = never timeout)`);
117
- // Determine port
118
- const port = pluginConfig.embedPort || Math.floor(Math.random() * 10000) + 10000;
119
- console.log(`[Hindsight] Port: ${port}`);
244
+ // Log dynamic bank ID mode
245
+ if (pluginConfig.dynamicBankId) {
246
+ const prefixInfo = pluginConfig.bankIdPrefix ? ` (prefix: ${pluginConfig.bankIdPrefix})` : '';
247
+ console.log(`[Hindsight] Dynamic bank IDs enabled${prefixInfo} - each channel gets isolated memory`);
248
+ }
249
+ else {
250
+ console.log(`[Hindsight] Dynamic bank IDs disabled - using static bank: ${DEFAULT_BANK_NAME}`);
251
+ }
252
+ // Detect external API mode
253
+ const externalApi = detectExternalApi(pluginConfig);
254
+ // Get API port from config (default: 9077)
255
+ const apiPort = pluginConfig.apiPort || 9077;
256
+ if (externalApi.apiUrl) {
257
+ // External API mode - skip local daemon
258
+ usingExternalApi = true;
259
+ console.log(`[Hindsight] ✓ Using external API: ${externalApi.apiUrl}`);
260
+ // Set env vars so CLI commands (uvx hindsight-embed) use external API
261
+ process.env.HINDSIGHT_EMBED_API_URL = externalApi.apiUrl;
262
+ if (externalApi.apiToken) {
263
+ process.env.HINDSIGHT_EMBED_API_TOKEN = externalApi.apiToken;
264
+ console.log('[Hindsight] API token configured');
265
+ }
266
+ }
267
+ else {
268
+ console.log(`[Hindsight] Daemon idle timeout: ${pluginConfig.daemonIdleTimeout}s (0 = never timeout)`);
269
+ console.log(`[Hindsight] API Port: ${apiPort}`);
270
+ }
120
271
  // Initialize in background (non-blocking)
121
272
  console.log('[Hindsight] Starting initialization in background...');
122
273
  initPromise = (async () => {
123
274
  try {
124
- // Initialize embed manager
125
- console.log('[Hindsight] Creating HindsightEmbedManager...');
126
- embedManager = new HindsightEmbedManager(port, llmConfig.provider, llmConfig.apiKey, llmConfig.model, llmConfig.baseUrl, pluginConfig.daemonIdleTimeout, pluginConfig.embedVersion);
127
- // Start the embedded server
128
- console.log('[Hindsight] Starting embedded server...');
129
- await embedManager.start();
130
- // Initialize client
131
- console.log('[Hindsight] Creating HindsightClient...');
132
- client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion);
133
- // Use openclaw bank
134
- console.log(`[Hindsight] Using bank: ${BANK_NAME}`);
135
- client.setBankId(BANK_NAME);
136
- // Set bank mission
137
- if (pluginConfig.bankMission) {
138
- console.log(`[Hindsight] Setting bank mission...`);
139
- await client.setBankMission(pluginConfig.bankMission);
275
+ if (usingExternalApi && externalApi.apiUrl) {
276
+ // External API mode - check health, skip daemon startup
277
+ console.log('[Hindsight] External API mode - skipping local daemon...');
278
+ await checkExternalApiHealth(externalApi.apiUrl);
279
+ // Initialize client (CLI commands will use external API via env vars)
280
+ console.log('[Hindsight] Creating HindsightClient...');
281
+ client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
282
+ // Set default bank (will be overridden per-request when dynamic bank IDs are enabled)
283
+ const defaultBankId = deriveBankId(undefined, pluginConfig);
284
+ console.log(`[Hindsight] Default bank: ${defaultBankId}`);
285
+ client.setBankId(defaultBankId);
286
+ // Note: Bank mission will be set per-bank when dynamic bank IDs are enabled
287
+ // For now, set it on the default bank
288
+ if (pluginConfig.bankMission && !pluginConfig.dynamicBankId) {
289
+ console.log(`[Hindsight] Setting bank mission...`);
290
+ await client.setBankMission(pluginConfig.bankMission);
291
+ }
292
+ isInitialized = true;
293
+ console.log('[Hindsight] ✓ Ready (external API mode)');
294
+ }
295
+ else {
296
+ // Local daemon mode - start hindsight-embed daemon
297
+ console.log('[Hindsight] Creating HindsightEmbedManager...');
298
+ embedManager = new HindsightEmbedManager(apiPort, llmConfig.provider, llmConfig.apiKey, llmConfig.model, llmConfig.baseUrl, pluginConfig.daemonIdleTimeout, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
299
+ // Start the embedded server
300
+ console.log('[Hindsight] Starting embedded server...');
301
+ await embedManager.start();
302
+ // Initialize client
303
+ console.log('[Hindsight] Creating HindsightClient...');
304
+ client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
305
+ // Set default bank (will be overridden per-request when dynamic bank IDs are enabled)
306
+ const defaultBankId = deriveBankId(undefined, pluginConfig);
307
+ console.log(`[Hindsight] Default bank: ${defaultBankId}`);
308
+ client.setBankId(defaultBankId);
309
+ // Note: Bank mission will be set per-bank when dynamic bank IDs are enabled
310
+ // For now, set it on the default bank
311
+ if (pluginConfig.bankMission && !pluginConfig.dynamicBankId) {
312
+ console.log(`[Hindsight] Setting bank mission...`);
313
+ await client.setBankMission(pluginConfig.bankMission);
314
+ }
315
+ isInitialized = true;
316
+ console.log('[Hindsight] ✓ Ready');
140
317
  }
141
- isInitialized = true;
142
- console.log('[Hindsight] ✓ Ready');
143
318
  }
144
319
  catch (error) {
145
320
  console.error('[Hindsight] Initialization error:', error);
@@ -152,7 +327,7 @@ export default function (api) {
152
327
  api.registerService({
153
328
  id: 'hindsight-memory',
154
329
  async start() {
155
- console.log('[Hindsight] Service start called - checking daemon health...');
330
+ console.log('[Hindsight] Service start called...');
156
331
  // Wait for background init if still pending
157
332
  if (initPromise) {
158
333
  try {
@@ -163,40 +338,83 @@ export default function (api) {
163
338
  // Continue to health check below
164
339
  }
165
340
  }
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;
341
+ // External API mode: check external API health
342
+ if (usingExternalApi) {
343
+ const externalApi = detectExternalApi(pluginConfig);
344
+ if (externalApi.apiUrl && isInitialized) {
345
+ try {
346
+ await checkExternalApiHealth(externalApi.apiUrl);
347
+ console.log('[Hindsight] External API is healthy');
348
+ return;
349
+ }
350
+ catch (error) {
351
+ console.error('[Hindsight] External API health check failed:', error);
352
+ // Reset state for reinitialization attempt
353
+ client = null;
354
+ isInitialized = false;
355
+ }
356
+ }
357
+ }
358
+ else {
359
+ // Local daemon mode: check daemon health (handles SIGUSR1 restart case)
360
+ if (embedManager && isInitialized) {
361
+ const healthy = await embedManager.checkHealth();
362
+ if (healthy) {
363
+ console.log('[Hindsight] Daemon is healthy');
364
+ return;
365
+ }
366
+ console.log('[Hindsight] Daemon is not responding - reinitializing...');
367
+ // Reset state for reinitialization
368
+ embedManager = null;
369
+ client = null;
370
+ isInitialized = false;
172
371
  }
173
- console.log('[Hindsight] Daemon is not responding - reinitializing...');
174
- // Reset state for reinitialization
175
- embedManager = null;
176
- client = null;
177
- isInitialized = false;
178
372
  }
179
- // Reinitialize if needed (fresh start or recovery from dead daemon)
373
+ // Reinitialize if needed (fresh start or recovery)
180
374
  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);
375
+ console.log('[Hindsight] Reinitializing...');
376
+ const reinitPluginConfig = getPluginConfig(api);
377
+ currentPluginConfig = reinitPluginConfig;
378
+ const llmConfig = detectLLMConfig(reinitPluginConfig);
379
+ const externalApi = detectExternalApi(reinitPluginConfig);
380
+ const apiPort = reinitPluginConfig.apiPort || 9077;
381
+ if (externalApi.apiUrl) {
382
+ // External API mode
383
+ usingExternalApi = true;
384
+ process.env.HINDSIGHT_EMBED_API_URL = externalApi.apiUrl;
385
+ if (externalApi.apiToken) {
386
+ process.env.HINDSIGHT_EMBED_API_TOKEN = externalApi.apiToken;
387
+ }
388
+ await checkExternalApiHealth(externalApi.apiUrl);
389
+ client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, reinitPluginConfig.embedVersion, reinitPluginConfig.embedPackagePath);
390
+ const defaultBankId = deriveBankId(undefined, reinitPluginConfig);
391
+ client.setBankId(defaultBankId);
392
+ if (reinitPluginConfig.bankMission && !reinitPluginConfig.dynamicBankId) {
393
+ await client.setBankMission(reinitPluginConfig.bankMission);
394
+ }
395
+ isInitialized = true;
396
+ console.log('[Hindsight] Reinitialization complete (external API mode)');
397
+ }
398
+ else {
399
+ // Local daemon mode
400
+ embedManager = new HindsightEmbedManager(apiPort, llmConfig.provider, llmConfig.apiKey, llmConfig.model, llmConfig.baseUrl, reinitPluginConfig.daemonIdleTimeout, reinitPluginConfig.embedVersion, reinitPluginConfig.embedPackagePath);
401
+ await embedManager.start();
402
+ client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, reinitPluginConfig.embedVersion, reinitPluginConfig.embedPackagePath);
403
+ const defaultBankId = deriveBankId(undefined, reinitPluginConfig);
404
+ client.setBankId(defaultBankId);
405
+ if (reinitPluginConfig.bankMission && !reinitPluginConfig.dynamicBankId) {
406
+ await client.setBankMission(reinitPluginConfig.bankMission);
407
+ }
408
+ isInitialized = true;
409
+ console.log('[Hindsight] Reinitialization complete');
191
410
  }
192
- isInitialized = true;
193
- console.log('[Hindsight] Reinitialization complete');
194
411
  }
195
412
  },
196
413
  async stop() {
197
414
  try {
198
415
  console.log('[Hindsight] Service stopping...');
199
- if (embedManager) {
416
+ // Only stop daemon if in local mode
417
+ if (!usingExternalApi && embedManager) {
200
418
  await embedManager.stop();
201
419
  embedManager = null;
202
420
  }
@@ -211,20 +429,25 @@ export default function (api) {
211
429
  },
212
430
  });
213
431
  console.log('[Hindsight] Plugin loaded successfully');
214
- // Register agent_end hook for auto-retention
215
- console.log('[Hindsight] Registering agent_end hook...');
216
- // Store session key for retention
432
+ // Register agent hooks for auto-recall and auto-retention
433
+ console.log('[Hindsight] Registering agent hooks...');
434
+ // Store session key and context for retention
217
435
  let currentSessionKey;
436
+ let currentAgentContext;
218
437
  // Auto-recall: Inject relevant memories before agent processes the message
219
- api.on('before_agent_start', async (context) => {
438
+ // Hook signature: (event, ctx) where event has {prompt, messages?} and ctx has agent context
439
+ api.on('before_agent_start', async (event, ctx) => {
220
440
  try {
221
- // Capture session key
222
- if (context.sessionKey) {
223
- currentSessionKey = context.sessionKey;
224
- console.log('[Hindsight] Captured session key:', currentSessionKey);
441
+ // Capture session key and context for use in agent_end
442
+ if (ctx?.sessionKey) {
443
+ currentSessionKey = ctx.sessionKey;
225
444
  }
445
+ currentAgentContext = ctx;
446
+ // Derive bank ID from context
447
+ const bankId = deriveBankId(ctx, pluginConfig);
448
+ console.log(`[Hindsight] before_agent_start - bank: ${bankId}, channel: ${ctx?.messageProvider}/${ctx?.channelId}`);
226
449
  // Get the user's latest message for recall
227
- let prompt = context.prompt;
450
+ let prompt = event.prompt;
228
451
  if (!prompt || typeof prompt !== 'string' || prompt.length < 5) {
229
452
  return; // Skip very short messages
230
453
  }
@@ -243,12 +466,13 @@ export default function (api) {
243
466
  return;
244
467
  }
245
468
  await clientGlobal.waitForReady();
246
- const client = clientGlobal.getClient();
469
+ // Get client configured for this context's bank (async to handle mission setup)
470
+ const client = await clientGlobal.getClientForContext(ctx);
247
471
  if (!client) {
248
472
  console.log('[Hindsight] Client not initialized, skipping auto-recall');
249
473
  return;
250
474
  }
251
- console.log('[Hindsight] Auto-recall for prompt:', prompt.substring(0, 50));
475
+ console.log(`[Hindsight] Auto-recall for bank ${bankId}, prompt: ${prompt.substring(0, 50)}`);
252
476
  // Recall relevant memories (up to 512 tokens)
253
477
  const response = await client.recall({
254
478
  query: prompt,
@@ -266,7 +490,7 @@ ${memoriesJson}
266
490
 
267
491
  User message: ${prompt}
268
492
  </hindsight_memories>`;
269
- console.log(`[Hindsight] Auto-recall: Injecting ${response.results.length} memories`);
493
+ console.log(`[Hindsight] Auto-recall: Injecting ${response.results.length} memories from bank ${bankId}`);
270
494
  // Inject context before the user message
271
495
  return { prependContext: contextMessage };
272
496
  }
@@ -275,9 +499,14 @@ User message: ${prompt}
275
499
  return;
276
500
  }
277
501
  });
278
- api.on('agent_end', async (event) => {
502
+ // Hook signature: (event, ctx) where event has {messages, success, error?, durationMs?}
503
+ api.on('agent_end', async (event, ctx) => {
279
504
  try {
280
- console.log('[Hindsight Hook] agent_end triggered');
505
+ // Use context from this hook, or fall back to context captured in before_agent_start
506
+ const effectiveCtx = ctx || currentAgentContext;
507
+ // Derive bank ID from context
508
+ const bankId = deriveBankId(effectiveCtx, pluginConfig);
509
+ console.log(`[Hindsight Hook] agent_end triggered - bank: ${bankId}`);
281
510
  // Check event success and messages
282
511
  if (!event.success || !Array.isArray(event.messages) || event.messages.length === 0) {
283
512
  console.log('[Hindsight Hook] Skipping: success:', event.success, 'messages:', event.messages?.length);
@@ -290,7 +519,8 @@ User message: ${prompt}
290
519
  return;
291
520
  }
292
521
  await clientGlobal.waitForReady();
293
- const client = clientGlobal.getClient();
522
+ // Get client configured for this context's bank (async to handle mission setup)
523
+ const client = await clientGlobal.getClientForContext(effectiveCtx);
294
524
  if (!client) {
295
525
  console.warn('[Hindsight] Client not initialized, skipping retain');
296
526
  return;
@@ -317,8 +547,8 @@ User message: ${prompt}
317
547
  console.log('[Hindsight Hook] Transcript too short, skipping');
318
548
  return;
319
549
  }
320
- // Use session key as document ID
321
- const documentId = currentSessionKey || 'default-session';
550
+ // Use session key as document ID (prefer context over captured value)
551
+ const documentId = effectiveCtx?.sessionKey || currentSessionKey || 'default-session';
322
552
  // Retain to Hindsight
323
553
  await client.retain({
324
554
  content: transcript,
@@ -326,15 +556,18 @@ User message: ${prompt}
326
556
  metadata: {
327
557
  retained_at: new Date().toISOString(),
328
558
  message_count: event.messages.length,
559
+ channel_type: effectiveCtx?.messageProvider,
560
+ channel_id: effectiveCtx?.channelId,
561
+ sender_id: effectiveCtx?.senderId,
329
562
  },
330
563
  });
331
- console.log(`[Hindsight] Retained ${event.messages.length} messages for session ${documentId}`);
564
+ console.log(`[Hindsight] Retained ${event.messages.length} messages to bank ${bankId} for session ${documentId}`);
332
565
  }
333
566
  catch (error) {
334
567
  console.error('[Hindsight] Error retaining messages:', error);
335
568
  }
336
569
  });
337
- console.log('[Hindsight] Hook registered');
570
+ console.log('[Hindsight] Hooks registered');
338
571
  }
339
572
  catch (error) {
340
573
  console.error('[Hindsight] Plugin loading error:', error);
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export interface MoltbotPluginAPI {
2
2
  config: MoltbotConfig;
3
3
  registerService(config: ServiceConfig): void;
4
- on(event: string, handler: (context: any) => void | Promise<void | {
4
+ on(event: string, handler: (event: any, ctx?: any) => void | Promise<void | {
5
5
  prependContext?: string;
6
6
  }>): void;
7
7
  }
@@ -29,6 +29,15 @@ export interface PluginConfig {
29
29
  embedPort?: number;
30
30
  daemonIdleTimeout?: number;
31
31
  embedVersion?: string;
32
+ embedPackagePath?: string;
33
+ llmProvider?: string;
34
+ llmModel?: string;
35
+ llmApiKeyEnv?: string;
36
+ apiPort?: number;
37
+ hindsightApiUrl?: string;
38
+ hindsightApiToken?: string;
39
+ dynamicBankId?: boolean;
40
+ bankIdPrefix?: string;
32
41
  }
33
42
  export interface ServiceConfig {
34
43
  id: string;
@@ -24,6 +24,46 @@
24
24
  "type": "string",
25
25
  "description": "hindsight-embed version to use (e.g. 'latest', '0.4.2', or empty for latest)",
26
26
  "default": "latest"
27
+ },
28
+ "llmProvider": {
29
+ "type": "string",
30
+ "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.",
31
+ "enum": ["openai", "anthropic", "gemini", "groq", "ollama", "openai-codex", "claude-code"]
32
+ },
33
+ "llmModel": {
34
+ "type": "string",
35
+ "description": "LLM model to use (e.g. 'gpt-4o-mini', 'claude-3-5-haiku-20241022'). Used with llmProvider."
36
+ },
37
+ "llmApiKeyEnv": {
38
+ "type": "string",
39
+ "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."
40
+ },
41
+ "embedPackagePath": {
42
+ "type": "string",
43
+ "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'."
44
+ },
45
+ "apiPort": {
46
+ "type": "number",
47
+ "description": "Port for the openclaw profile daemon (default: 9077)",
48
+ "default": 9077
49
+ },
50
+ "hindsightApiUrl": {
51
+ "type": "string",
52
+ "description": "External Hindsight API URL (e.g. 'https://mcp.hindsight.devcraft.team'). When set, skips local daemon and connects directly to this API.",
53
+ "format": "uri"
54
+ },
55
+ "hindsightApiToken": {
56
+ "type": "string",
57
+ "description": "API token for external Hindsight API authentication. Required if the external API has authentication enabled."
58
+ },
59
+ "dynamicBankId": {
60
+ "type": "boolean",
61
+ "description": "Enable per-channel memory banks. When true, memories are isolated by channel (e.g., slack-C123, telegram-456). When false, all channels share a single 'openclaw' bank.",
62
+ "default": true
63
+ },
64
+ "bankIdPrefix": {
65
+ "type": "string",
66
+ "description": "Optional prefix for bank IDs (e.g., 'prod' results in 'prod-slack-C123'). Useful for separating environments."
27
67
  }
28
68
  },
29
69
  "additionalProperties": false
@@ -44,6 +84,42 @@
44
84
  "embedVersion": {
45
85
  "label": "Hindsight Embed Version",
46
86
  "placeholder": "latest (or pin to specific version like 0.4.2)"
87
+ },
88
+ "llmProvider": {
89
+ "label": "LLM Provider",
90
+ "placeholder": "e.g. openai, anthropic, gemini, groq"
91
+ },
92
+ "llmModel": {
93
+ "label": "LLM Model",
94
+ "placeholder": "e.g. gpt-4o-mini, claude-3-5-haiku-20241022"
95
+ },
96
+ "llmApiKeyEnv": {
97
+ "label": "API Key Env Var",
98
+ "placeholder": "e.g. MY_CUSTOM_API_KEY (optional)"
99
+ },
100
+ "embedPackagePath": {
101
+ "label": "Local Package Path (Dev)",
102
+ "placeholder": "/path/to/hindsight (for local development)"
103
+ },
104
+ "apiPort": {
105
+ "label": "API Port",
106
+ "placeholder": "9077 (default)"
107
+ },
108
+ "hindsightApiUrl": {
109
+ "label": "External Hindsight API URL",
110
+ "placeholder": "e.g. https://mcp.hindsight.devcraft.team (leave empty for local daemon)"
111
+ },
112
+ "hindsightApiToken": {
113
+ "label": "External API Token",
114
+ "placeholder": "API token if external API requires authentication"
115
+ },
116
+ "dynamicBankId": {
117
+ "label": "Dynamic Bank IDs",
118
+ "placeholder": "true (isolate memories per channel)"
119
+ },
120
+ "bankIdPrefix": {
121
+ "label": "Bank ID Prefix",
122
+ "placeholder": "e.g., prod, staging (optional)"
47
123
  }
48
124
  }
49
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectorize-io/hindsight-openclaw",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
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",