@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 +42 -2
- package/dist/client.d.ts +29 -2
- package/dist/client.js +58 -25
- package/dist/embed-manager.d.ts +8 -2
- package/dist/embed-manager.js +80 -78
- package/dist/index.js +315 -82
- package/dist/types.d.ts +10 -1
- package/openclaw.plugin.json +76 -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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
24
|
-
const
|
|
25
|
-
const cmd =
|
|
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
|
|
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
|
|
48
|
-
const docId = request.document_id || 'conversation';
|
|
49
|
-
const
|
|
50
|
-
const cmd =
|
|
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
|
|
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
|
|
99
|
+
const query = escapeShellArg(request.query);
|
|
67
100
|
const maxTokens = request.max_tokens || 1024;
|
|
68
|
-
const
|
|
69
|
-
const cmd =
|
|
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
|
|
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 || [];
|
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
|
@@ -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
|
|
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
|
|
94
|
+
// Priority 1: If provider is explicitly set via env var, use that
|
|
42
95
|
if (overrideProvider) {
|
|
43
|
-
|
|
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
|
-
//
|
|
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
|
|
60
|
-
|
|
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.
|
|
79
|
-
` export GROQ_API_KEY=your-key # Uses
|
|
80
|
-
`Option 2:
|
|
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
|
-
//
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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
|
-
//
|
|
167
|
-
if (
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
373
|
+
// Reinitialize if needed (fresh start or recovery)
|
|
180
374
|
if (!isInitialized) {
|
|
181
|
-
console.log('[Hindsight] Reinitializing
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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
|
|
215
|
-
console.log('[Hindsight] Registering
|
|
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
|
-
|
|
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 (
|
|
223
|
-
currentSessionKey =
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
502
|
+
// Hook signature: (event, ctx) where event has {messages, success, error?, durationMs?}
|
|
503
|
+
api.on('agent_end', async (event, ctx) => {
|
|
279
504
|
try {
|
|
280
|
-
|
|
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
|
-
|
|
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]
|
|
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: (
|
|
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;
|
package/openclaw.plugin.json
CHANGED
|
@@ -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