@vectorize-io/hindsight-openclaw 0.4.7 → 0.4.8
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 +42 -2
- package/dist/client.d.ts +6 -2
- package/dist/client.js +26 -21
- package/dist/embed-manager.d.ts +8 -2
- package/dist/embed-manager.js +80 -78
- package/dist/index.js +64 -22
- package/dist/types.d.ts +5 -0
- package/openclaw.plugin.json +42 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
@@ -5,10 +5,14 @@ export declare class HindsightClient {
|
|
|
5
5
|
private llmApiKey;
|
|
6
6
|
private llmModel?;
|
|
7
7
|
private embedVersion;
|
|
8
|
-
|
|
8
|
+
private embedPackagePath?;
|
|
9
|
+
constructor(llmProvider: string, llmApiKey: string, llmModel?: string, embedVersion?: string, embedPackagePath?: string);
|
|
10
|
+
/**
|
|
11
|
+
* Get the command prefix to run hindsight-embed (either local or from PyPI)
|
|
12
|
+
*/
|
|
13
|
+
private getEmbedCommandPrefix;
|
|
9
14
|
setBankId(bankId: string): void;
|
|
10
15
|
setBankMission(mission: string): Promise<void>;
|
|
11
|
-
private getEnv;
|
|
12
16
|
retain(request: RetainRequest): Promise<RetainResponse>;
|
|
13
17
|
recall(request: RecallRequest): Promise<RecallResponse>;
|
|
14
18
|
}
|
package/dist/client.js
CHANGED
|
@@ -7,11 +7,27 @@ export class HindsightClient {
|
|
|
7
7
|
llmApiKey;
|
|
8
8
|
llmModel;
|
|
9
9
|
embedVersion;
|
|
10
|
-
|
|
10
|
+
embedPackagePath;
|
|
11
|
+
constructor(llmProvider, llmApiKey, llmModel, embedVersion = 'latest', embedPackagePath) {
|
|
11
12
|
this.llmProvider = llmProvider;
|
|
12
13
|
this.llmApiKey = llmApiKey;
|
|
13
14
|
this.llmModel = llmModel;
|
|
14
15
|
this.embedVersion = embedVersion || 'latest';
|
|
16
|
+
this.embedPackagePath = embedPackagePath;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get the command prefix to run hindsight-embed (either local or from PyPI)
|
|
20
|
+
*/
|
|
21
|
+
getEmbedCommandPrefix() {
|
|
22
|
+
if (this.embedPackagePath) {
|
|
23
|
+
// Local package: uv run --directory <path> hindsight-embed
|
|
24
|
+
return `uv run --directory ${this.embedPackagePath} hindsight-embed`;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// PyPI package: uvx hindsight-embed@version
|
|
28
|
+
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
|
|
29
|
+
return `uvx ${embedPackage}`;
|
|
30
|
+
}
|
|
15
31
|
}
|
|
16
32
|
setBankId(bankId) {
|
|
17
33
|
this.bankId = bankId;
|
|
@@ -21,10 +37,10 @@ export class HindsightClient {
|
|
|
21
37
|
return;
|
|
22
38
|
}
|
|
23
39
|
const escapedMission = mission.replace(/'/g, "'\\''"); // Escape single quotes
|
|
24
|
-
const
|
|
25
|
-
const cmd =
|
|
40
|
+
const embedCmd = this.getEmbedCommandPrefix();
|
|
41
|
+
const cmd = `${embedCmd} --profile openclaw bank mission ${this.bankId} '${escapedMission}'`;
|
|
26
42
|
try {
|
|
27
|
-
const { stdout } = await execAsync(cmd
|
|
43
|
+
const { stdout } = await execAsync(cmd);
|
|
28
44
|
console.log(`[Hindsight] Bank mission set: ${stdout.trim()}`);
|
|
29
45
|
}
|
|
30
46
|
catch (error) {
|
|
@@ -32,24 +48,13 @@ export class HindsightClient {
|
|
|
32
48
|
console.warn(`[Hindsight] Could not set bank mission (bank may not exist yet): ${error}`);
|
|
33
49
|
}
|
|
34
50
|
}
|
|
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
51
|
async retain(request) {
|
|
47
52
|
const content = request.content.replace(/'/g, "'\\''"); // Escape single quotes
|
|
48
53
|
const docId = request.document_id || 'conversation';
|
|
49
|
-
const
|
|
50
|
-
const cmd =
|
|
54
|
+
const embedCmd = this.getEmbedCommandPrefix();
|
|
55
|
+
const cmd = `${embedCmd} --profile openclaw memory retain ${this.bankId} '${content}' --doc-id '${docId}' --async`;
|
|
51
56
|
try {
|
|
52
|
-
const { stdout } = await execAsync(cmd
|
|
57
|
+
const { stdout } = await execAsync(cmd);
|
|
53
58
|
console.log(`[Hindsight] Retained (async): ${stdout.trim()}`);
|
|
54
59
|
// Return a simple response
|
|
55
60
|
return {
|
|
@@ -65,10 +70,10 @@ export class HindsightClient {
|
|
|
65
70
|
async recall(request) {
|
|
66
71
|
const query = request.query.replace(/'/g, "'\\''"); // Escape single quotes
|
|
67
72
|
const maxTokens = request.max_tokens || 1024;
|
|
68
|
-
const
|
|
69
|
-
const cmd =
|
|
73
|
+
const embedCmd = this.getEmbedCommandPrefix();
|
|
74
|
+
const cmd = `${embedCmd} --profile openclaw memory recall ${this.bankId} '${query}' --output json --max-tokens ${maxTokens}`;
|
|
70
75
|
try {
|
|
71
|
-
const { stdout } = await execAsync(cmd
|
|
76
|
+
const { stdout } = await execAsync(cmd);
|
|
72
77
|
// Parse JSON output - returns { entities: {...}, results: [...] }
|
|
73
78
|
const response = JSON.parse(stdout);
|
|
74
79
|
const results = response.results || [];
|
package/dist/embed-manager.d.ts
CHANGED
|
@@ -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
|
|
26
|
+
private configureProfile;
|
|
21
27
|
}
|
package/dist/embed-manager.js
CHANGED
|
@@ -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
|
-
|
|
20
|
-
this.
|
|
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
|
-
|
|
35
|
-
|
|
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['
|
|
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
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
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
|
|
92
|
-
const stopDaemon = spawn(
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
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
|
@@ -31,16 +31,20 @@ const PROVIDER_DETECTION = [
|
|
|
31
31
|
{ name: 'gemini', keyEnv: 'GEMINI_API_KEY', defaultModel: 'gemini-2.5-flash' },
|
|
32
32
|
{ name: 'groq', keyEnv: 'GROQ_API_KEY', defaultModel: 'openai/gpt-oss-20b' },
|
|
33
33
|
{ name: 'ollama', keyEnv: '', defaultModel: 'llama3.2' },
|
|
34
|
+
{ name: 'openai-codex', keyEnv: '', defaultModel: 'gpt-5.2-codex' },
|
|
35
|
+
{ name: 'claude-code', keyEnv: '', defaultModel: 'claude-sonnet-4-5-20250929' },
|
|
34
36
|
];
|
|
35
|
-
function detectLLMConfig() {
|
|
37
|
+
function detectLLMConfig(pluginConfig) {
|
|
36
38
|
// Override values from HINDSIGHT_API_LLM_* env vars (highest priority)
|
|
37
39
|
const overrideProvider = process.env.HINDSIGHT_API_LLM_PROVIDER;
|
|
38
40
|
const overrideModel = process.env.HINDSIGHT_API_LLM_MODEL;
|
|
39
41
|
const overrideKey = process.env.HINDSIGHT_API_LLM_API_KEY;
|
|
40
42
|
const overrideBaseUrl = process.env.HINDSIGHT_API_LLM_BASE_URL;
|
|
41
|
-
// If provider is explicitly set, use that
|
|
43
|
+
// Priority 1: If provider is explicitly set via env var, use that
|
|
42
44
|
if (overrideProvider) {
|
|
43
|
-
|
|
45
|
+
// Providers that don't require an API key (use OAuth or local models)
|
|
46
|
+
const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
|
|
47
|
+
if (!overrideKey && !noKeyRequired.includes(overrideProvider)) {
|
|
44
48
|
throw new Error(`HINDSIGHT_API_LLM_PROVIDER is set to "${overrideProvider}" but HINDSIGHT_API_LLM_API_KEY is not set.\n` +
|
|
45
49
|
`Please set: export HINDSIGHT_API_LLM_API_KEY=your-api-key`);
|
|
46
50
|
}
|
|
@@ -53,11 +57,39 @@ function detectLLMConfig() {
|
|
|
53
57
|
source: 'HINDSIGHT_API_LLM_PROVIDER override',
|
|
54
58
|
};
|
|
55
59
|
}
|
|
56
|
-
//
|
|
60
|
+
// Priority 2: Plugin config llmProvider/llmModel
|
|
61
|
+
if (pluginConfig?.llmProvider) {
|
|
62
|
+
const providerInfo = PROVIDER_DETECTION.find(p => p.name === pluginConfig.llmProvider);
|
|
63
|
+
// Resolve API key: llmApiKeyEnv > provider's standard keyEnv
|
|
64
|
+
let apiKey = '';
|
|
65
|
+
if (pluginConfig.llmApiKeyEnv) {
|
|
66
|
+
apiKey = process.env[pluginConfig.llmApiKeyEnv] || '';
|
|
67
|
+
}
|
|
68
|
+
else if (providerInfo?.keyEnv) {
|
|
69
|
+
apiKey = process.env[providerInfo.keyEnv] || '';
|
|
70
|
+
}
|
|
71
|
+
// Providers that don't require an API key (use OAuth or local models)
|
|
72
|
+
const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
|
|
73
|
+
if (!apiKey && !noKeyRequired.includes(pluginConfig.llmProvider)) {
|
|
74
|
+
const keySource = pluginConfig.llmApiKeyEnv || providerInfo?.keyEnv || 'unknown';
|
|
75
|
+
throw new Error(`Plugin config llmProvider is set to "${pluginConfig.llmProvider}" but no API key found.\n` +
|
|
76
|
+
`Expected env var: ${keySource}\n` +
|
|
77
|
+
`Set the env var or use llmApiKeyEnv in plugin config to specify a custom env var name.`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
provider: pluginConfig.llmProvider,
|
|
81
|
+
apiKey,
|
|
82
|
+
model: pluginConfig.llmModel || overrideModel || providerInfo?.defaultModel,
|
|
83
|
+
baseUrl: overrideBaseUrl,
|
|
84
|
+
source: 'plugin config',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Priority 3: Auto-detect from standard provider env vars
|
|
57
88
|
for (const providerInfo of PROVIDER_DETECTION) {
|
|
58
89
|
const apiKey = providerInfo.keyEnv ? process.env[providerInfo.keyEnv] : '';
|
|
59
|
-
// Skip
|
|
60
|
-
|
|
90
|
+
// Skip providers that don't use API keys in auto-detection (must be explicitly requested)
|
|
91
|
+
const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
|
|
92
|
+
if (noKeyRequired.includes(providerInfo.name)) {
|
|
61
93
|
continue;
|
|
62
94
|
}
|
|
63
95
|
if (apiKey) {
|
|
@@ -75,9 +107,14 @@ function detectLLMConfig() {
|
|
|
75
107
|
`Option 1: Set a standard provider API key (auto-detect):\n` +
|
|
76
108
|
` export OPENAI_API_KEY=sk-your-key # Uses gpt-4o-mini\n` +
|
|
77
109
|
` export ANTHROPIC_API_KEY=your-key # Uses claude-3-5-haiku\n` +
|
|
78
|
-
` export GEMINI_API_KEY=your-key # Uses gemini-2.
|
|
79
|
-
` export GROQ_API_KEY=your-key # Uses
|
|
80
|
-
`Option 2:
|
|
110
|
+
` export GEMINI_API_KEY=your-key # Uses gemini-2.5-flash\n` +
|
|
111
|
+
` export GROQ_API_KEY=your-key # Uses openai/gpt-oss-20b\n\n` +
|
|
112
|
+
`Option 2: Use Codex or Claude Code (no API key needed):\n` +
|
|
113
|
+
` export HINDSIGHT_API_LLM_PROVIDER=openai-codex # Requires 'codex auth login'\n` +
|
|
114
|
+
` export HINDSIGHT_API_LLM_PROVIDER=claude-code # Requires Claude Code CLI\n\n` +
|
|
115
|
+
`Option 3: Set llmProvider in openclaw.json plugin config:\n` +
|
|
116
|
+
` "llmProvider": "openai", "llmModel": "gpt-4o-mini"\n\n` +
|
|
117
|
+
`Option 4: Override with Hindsight-specific env vars:\n` +
|
|
81
118
|
` export HINDSIGHT_API_LLM_PROVIDER=openai\n` +
|
|
82
119
|
` export HINDSIGHT_API_LLM_MODEL=gpt-4o-mini\n` +
|
|
83
120
|
` export HINDSIGHT_API_LLM_API_KEY=sk-your-key\n` +
|
|
@@ -92,14 +129,21 @@ function getPluginConfig(api) {
|
|
|
92
129
|
embedPort: config.embedPort || 0,
|
|
93
130
|
daemonIdleTimeout: config.daemonIdleTimeout !== undefined ? config.daemonIdleTimeout : 0,
|
|
94
131
|
embedVersion: config.embedVersion || 'latest',
|
|
132
|
+
embedPackagePath: config.embedPackagePath,
|
|
133
|
+
llmProvider: config.llmProvider,
|
|
134
|
+
llmModel: config.llmModel,
|
|
135
|
+
llmApiKeyEnv: config.llmApiKeyEnv,
|
|
95
136
|
};
|
|
96
137
|
}
|
|
97
138
|
export default function (api) {
|
|
98
139
|
try {
|
|
99
140
|
console.log('[Hindsight] Plugin loading...');
|
|
100
|
-
//
|
|
141
|
+
// Get plugin config first (needed for LLM detection)
|
|
142
|
+
console.log('[Hindsight] Getting plugin config...');
|
|
143
|
+
const pluginConfig = getPluginConfig(api);
|
|
144
|
+
// Detect LLM configuration (env vars > plugin config > auto-detect)
|
|
101
145
|
console.log('[Hindsight] Detecting LLM config...');
|
|
102
|
-
const llmConfig = detectLLMConfig();
|
|
146
|
+
const llmConfig = detectLLMConfig(pluginConfig);
|
|
103
147
|
const baseUrlInfo = llmConfig.baseUrl ? `, base URL: ${llmConfig.baseUrl}` : '';
|
|
104
148
|
const modelInfo = llmConfig.model || 'default';
|
|
105
149
|
if (llmConfig.provider === 'ollama') {
|
|
@@ -108,28 +152,26 @@ export default function (api) {
|
|
|
108
152
|
else {
|
|
109
153
|
console.log(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${modelInfo} (${llmConfig.source}${baseUrlInfo})`);
|
|
110
154
|
}
|
|
111
|
-
console.log('[Hindsight] Getting plugin config...');
|
|
112
|
-
const pluginConfig = getPluginConfig(api);
|
|
113
155
|
if (pluginConfig.bankMission) {
|
|
114
156
|
console.log(`[Hindsight] Custom bank mission configured: "${pluginConfig.bankMission.substring(0, 50)}..."`);
|
|
115
157
|
}
|
|
116
158
|
console.log(`[Hindsight] Daemon idle timeout: ${pluginConfig.daemonIdleTimeout}s (0 = never timeout)`);
|
|
117
|
-
//
|
|
118
|
-
const
|
|
119
|
-
console.log(`[Hindsight] Port: ${
|
|
159
|
+
// Get API port from config (default: 9077)
|
|
160
|
+
const apiPort = pluginConfig.apiPort || 9077;
|
|
161
|
+
console.log(`[Hindsight] API Port: ${apiPort}`);
|
|
120
162
|
// Initialize in background (non-blocking)
|
|
121
163
|
console.log('[Hindsight] Starting initialization in background...');
|
|
122
164
|
initPromise = (async () => {
|
|
123
165
|
try {
|
|
124
166
|
// Initialize embed manager
|
|
125
167
|
console.log('[Hindsight] Creating HindsightEmbedManager...');
|
|
126
|
-
embedManager = new HindsightEmbedManager(
|
|
168
|
+
embedManager = new HindsightEmbedManager(apiPort, llmConfig.provider, llmConfig.apiKey, llmConfig.model, llmConfig.baseUrl, pluginConfig.daemonIdleTimeout, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
|
|
127
169
|
// Start the embedded server
|
|
128
170
|
console.log('[Hindsight] Starting embedded server...');
|
|
129
171
|
await embedManager.start();
|
|
130
172
|
// Initialize client
|
|
131
173
|
console.log('[Hindsight] Creating HindsightClient...');
|
|
132
|
-
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion);
|
|
174
|
+
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
|
|
133
175
|
// Use openclaw bank
|
|
134
176
|
console.log(`[Hindsight] Using bank: ${BANK_NAME}`);
|
|
135
177
|
client.setBankId(BANK_NAME);
|
|
@@ -179,12 +221,12 @@ export default function (api) {
|
|
|
179
221
|
// Reinitialize if needed (fresh start or recovery from dead daemon)
|
|
180
222
|
if (!isInitialized) {
|
|
181
223
|
console.log('[Hindsight] Reinitializing daemon...');
|
|
182
|
-
const llmConfig = detectLLMConfig();
|
|
183
224
|
const pluginConfig = getPluginConfig(api);
|
|
184
|
-
const
|
|
185
|
-
|
|
225
|
+
const llmConfig = detectLLMConfig(pluginConfig);
|
|
226
|
+
const apiPort = pluginConfig.apiPort || 9077;
|
|
227
|
+
embedManager = new HindsightEmbedManager(apiPort, llmConfig.provider, llmConfig.apiKey, llmConfig.model, llmConfig.baseUrl, pluginConfig.daemonIdleTimeout, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
|
|
186
228
|
await embedManager.start();
|
|
187
|
-
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion);
|
|
229
|
+
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
|
|
188
230
|
client.setBankId(BANK_NAME);
|
|
189
231
|
if (pluginConfig.bankMission) {
|
|
190
232
|
await client.setBankMission(pluginConfig.bankMission);
|
package/dist/types.d.ts
CHANGED
|
@@ -29,6 +29,11 @@ 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;
|
|
32
37
|
}
|
|
33
38
|
export interface ServiceConfig {
|
|
34
39
|
id: string;
|
package/openclaw.plugin.json
CHANGED
|
@@ -24,6 +24,28 @@
|
|
|
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
|
|
27
49
|
}
|
|
28
50
|
},
|
|
29
51
|
"additionalProperties": false
|
|
@@ -44,6 +66,26 @@
|
|
|
44
66
|
"embedVersion": {
|
|
45
67
|
"label": "Hindsight Embed Version",
|
|
46
68
|
"placeholder": "latest (or pin to specific version like 0.4.2)"
|
|
69
|
+
},
|
|
70
|
+
"llmProvider": {
|
|
71
|
+
"label": "LLM Provider",
|
|
72
|
+
"placeholder": "e.g. openai, anthropic, gemini, groq"
|
|
73
|
+
},
|
|
74
|
+
"llmModel": {
|
|
75
|
+
"label": "LLM Model",
|
|
76
|
+
"placeholder": "e.g. gpt-4o-mini, claude-3-5-haiku-20241022"
|
|
77
|
+
},
|
|
78
|
+
"llmApiKeyEnv": {
|
|
79
|
+
"label": "API Key Env Var",
|
|
80
|
+
"placeholder": "e.g. MY_CUSTOM_API_KEY (optional)"
|
|
81
|
+
},
|
|
82
|
+
"embedPackagePath": {
|
|
83
|
+
"label": "Local Package Path (Dev)",
|
|
84
|
+
"placeholder": "/path/to/hindsight (for local development)"
|
|
85
|
+
},
|
|
86
|
+
"apiPort": {
|
|
87
|
+
"label": "API Port",
|
|
88
|
+
"placeholder": "9077 (default)"
|
|
47
89
|
}
|
|
48
90
|
}
|
|
49
91
|
}
|
package/package.json
CHANGED