@vectorize-io/hindsight-openclaw 0.4.8 → 0.4.10
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/dist/client.d.ts +23 -0
- package/dist/client.js +32 -4
- package/dist/index.js +278 -73
- package/dist/types.d.ts +5 -1
- package/openclaw.plugin.json +33 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
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;
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
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;
|
|
@@ -36,7 +64,7 @@ export class HindsightClient {
|
|
|
36
64
|
if (!mission || mission.trim().length === 0) {
|
|
37
65
|
return;
|
|
38
66
|
}
|
|
39
|
-
const escapedMission = mission
|
|
67
|
+
const escapedMission = escapeShellArg(mission);
|
|
40
68
|
const embedCmd = this.getEmbedCommandPrefix();
|
|
41
69
|
const cmd = `${embedCmd} --profile openclaw bank mission ${this.bankId} '${escapedMission}'`;
|
|
42
70
|
try {
|
|
@@ -49,8 +77,8 @@ export class HindsightClient {
|
|
|
49
77
|
}
|
|
50
78
|
}
|
|
51
79
|
async retain(request) {
|
|
52
|
-
const content = request.content
|
|
53
|
-
const docId = request.document_id || 'conversation';
|
|
80
|
+
const content = escapeShellArg(request.content);
|
|
81
|
+
const docId = escapeShellArg(request.document_id || 'conversation');
|
|
54
82
|
const embedCmd = this.getEmbedCommandPrefix();
|
|
55
83
|
const cmd = `${embedCmd} --profile openclaw memory retain ${this.bankId} '${content}' --doc-id '${docId}' --async`;
|
|
56
84
|
try {
|
|
@@ -68,7 +96,7 @@ export class HindsightClient {
|
|
|
68
96
|
}
|
|
69
97
|
}
|
|
70
98
|
async recall(request) {
|
|
71
|
-
const query = request.query
|
|
99
|
+
const query = escapeShellArg(request.query);
|
|
72
100
|
const maxTokens = request.max_tokens || 1024;
|
|
73
101
|
const embedCmd = this.getEmbedCommandPrefix();
|
|
74
102
|
const cmd = `${embedCmd} --profile openclaw memory recall ${this.bankId} '${query}' --output json --max-tokens ${maxTokens}`;
|
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' },
|
|
@@ -121,6 +172,33 @@ function detectLLMConfig(pluginConfig) {
|
|
|
121
172
|
` export HINDSIGHT_API_LLM_BASE_URL=https://openrouter.ai/api/v1 # Optional\n\n` +
|
|
122
173
|
`Tip: Use a cheap/fast model for memory extraction (e.g., gpt-4o-mini, claude-3-5-haiku, or free models on OpenRouter)`);
|
|
123
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
|
+
}
|
|
124
202
|
function getPluginConfig(api) {
|
|
125
203
|
const config = api.config.plugins?.entries?.['hindsight-openclaw']?.config || {};
|
|
126
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.';
|
|
@@ -133,6 +211,12 @@ function getPluginConfig(api) {
|
|
|
133
211
|
llmProvider: config.llmProvider,
|
|
134
212
|
llmModel: config.llmModel,
|
|
135
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,
|
|
136
220
|
};
|
|
137
221
|
}
|
|
138
222
|
export default function (api) {
|
|
@@ -141,6 +225,8 @@ export default function (api) {
|
|
|
141
225
|
// Get plugin config first (needed for LLM detection)
|
|
142
226
|
console.log('[Hindsight] Getting plugin config...');
|
|
143
227
|
const pluginConfig = getPluginConfig(api);
|
|
228
|
+
// Store config globally for bank ID derivation in hooks
|
|
229
|
+
currentPluginConfig = pluginConfig;
|
|
144
230
|
// Detect LLM configuration (env vars > plugin config > auto-detect)
|
|
145
231
|
console.log('[Hindsight] Detecting LLM config...');
|
|
146
232
|
const llmConfig = detectLLMConfig(pluginConfig);
|
|
@@ -155,33 +241,80 @@ export default function (api) {
|
|
|
155
241
|
if (pluginConfig.bankMission) {
|
|
156
242
|
console.log(`[Hindsight] Custom bank mission configured: "${pluginConfig.bankMission.substring(0, 50)}..."`);
|
|
157
243
|
}
|
|
158
|
-
|
|
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);
|
|
159
254
|
// Get API port from config (default: 9077)
|
|
160
255
|
const apiPort = pluginConfig.apiPort || 9077;
|
|
161
|
-
|
|
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
|
+
}
|
|
162
271
|
// Initialize in background (non-blocking)
|
|
163
272
|
console.log('[Hindsight] Starting initialization in background...');
|
|
164
273
|
initPromise = (async () => {
|
|
165
274
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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');
|
|
182
317
|
}
|
|
183
|
-
isInitialized = true;
|
|
184
|
-
console.log('[Hindsight] ✓ Ready');
|
|
185
318
|
}
|
|
186
319
|
catch (error) {
|
|
187
320
|
console.error('[Hindsight] Initialization error:', error);
|
|
@@ -194,7 +327,7 @@ export default function (api) {
|
|
|
194
327
|
api.registerService({
|
|
195
328
|
id: 'hindsight-memory',
|
|
196
329
|
async start() {
|
|
197
|
-
console.log('[Hindsight] Service start called
|
|
330
|
+
console.log('[Hindsight] Service start called...');
|
|
198
331
|
// Wait for background init if still pending
|
|
199
332
|
if (initPromise) {
|
|
200
333
|
try {
|
|
@@ -205,40 +338,83 @@ export default function (api) {
|
|
|
205
338
|
// Continue to health check below
|
|
206
339
|
}
|
|
207
340
|
}
|
|
208
|
-
//
|
|
209
|
-
if (
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
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;
|
|
214
371
|
}
|
|
215
|
-
console.log('[Hindsight] Daemon is not responding - reinitializing...');
|
|
216
|
-
// Reset state for reinitialization
|
|
217
|
-
embedManager = null;
|
|
218
|
-
client = null;
|
|
219
|
-
isInitialized = false;
|
|
220
372
|
}
|
|
221
|
-
// Reinitialize if needed (fresh start or recovery
|
|
373
|
+
// Reinitialize if needed (fresh start or recovery)
|
|
222
374
|
if (!isInitialized) {
|
|
223
|
-
console.log('[Hindsight] Reinitializing
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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');
|
|
233
410
|
}
|
|
234
|
-
isInitialized = true;
|
|
235
|
-
console.log('[Hindsight] Reinitialization complete');
|
|
236
411
|
}
|
|
237
412
|
},
|
|
238
413
|
async stop() {
|
|
239
414
|
try {
|
|
240
415
|
console.log('[Hindsight] Service stopping...');
|
|
241
|
-
if
|
|
416
|
+
// Only stop daemon if in local mode
|
|
417
|
+
if (!usingExternalApi && embedManager) {
|
|
242
418
|
await embedManager.stop();
|
|
243
419
|
embedManager = null;
|
|
244
420
|
}
|
|
@@ -253,28 +429,46 @@ export default function (api) {
|
|
|
253
429
|
},
|
|
254
430
|
});
|
|
255
431
|
console.log('[Hindsight] Plugin loaded successfully');
|
|
256
|
-
// Register
|
|
257
|
-
console.log('[Hindsight] Registering
|
|
258
|
-
// 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
|
|
259
435
|
let currentSessionKey;
|
|
436
|
+
let currentAgentContext;
|
|
260
437
|
// Auto-recall: Inject relevant memories before agent processes the message
|
|
261
|
-
|
|
438
|
+
// Hook signature: (event, ctx) where event has {prompt, messages?} and ctx has agent context
|
|
439
|
+
api.on('before_agent_start', async (event, ctx) => {
|
|
262
440
|
try {
|
|
263
|
-
// Capture session key
|
|
264
|
-
if (
|
|
265
|
-
currentSessionKey =
|
|
266
|
-
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;
|
|
267
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}`);
|
|
268
449
|
// Get the user's latest message for recall
|
|
269
|
-
|
|
450
|
+
// Prefer rawMessage (clean user text) over prompt (envelope-formatted)
|
|
451
|
+
let prompt = event.rawMessage ?? event.prompt;
|
|
270
452
|
if (!prompt || typeof prompt !== 'string' || prompt.length < 5) {
|
|
271
453
|
return; // Skip very short messages
|
|
272
454
|
}
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
455
|
+
// Strip envelope-formatted prompts from any channel
|
|
456
|
+
// The prompt may contain: System: lines, abort hints, [Channel ...] header, [from: ...] suffix
|
|
457
|
+
let cleaned = prompt;
|
|
458
|
+
// Remove leading "System: ..." lines (from prependSystemEvents)
|
|
459
|
+
cleaned = cleaned.replace(/^(?:System:.*\n)+\n?/, '');
|
|
460
|
+
// Remove session abort hint
|
|
461
|
+
cleaned = cleaned.replace(/^Note: The previous agent run was aborted[^\n]*\n\n/, '');
|
|
462
|
+
// Extract message after [ChannelName ...] envelope header
|
|
463
|
+
// Handles any channel: Telegram, Slack, Discord, WhatsApp, Signal, etc.
|
|
464
|
+
// Uses [\s\S]+ instead of .+ to support multiline messages
|
|
465
|
+
const envelopeMatch = cleaned.match(/\[[A-Z][A-Za-z]*(?:\s[^\]]+)?\]\s*([\s\S]+)$/);
|
|
466
|
+
if (envelopeMatch) {
|
|
467
|
+
cleaned = envelopeMatch[1];
|
|
277
468
|
}
|
|
469
|
+
// Remove trailing [from: SenderName] metadata (group chats)
|
|
470
|
+
cleaned = cleaned.replace(/\n\[from:[^\]]*\]\s*$/, '');
|
|
471
|
+
prompt = cleaned.trim() || prompt;
|
|
278
472
|
if (prompt.length < 5) {
|
|
279
473
|
return; // Skip very short messages after extraction
|
|
280
474
|
}
|
|
@@ -285,16 +479,17 @@ export default function (api) {
|
|
|
285
479
|
return;
|
|
286
480
|
}
|
|
287
481
|
await clientGlobal.waitForReady();
|
|
288
|
-
|
|
482
|
+
// Get client configured for this context's bank (async to handle mission setup)
|
|
483
|
+
const client = await clientGlobal.getClientForContext(ctx);
|
|
289
484
|
if (!client) {
|
|
290
485
|
console.log('[Hindsight] Client not initialized, skipping auto-recall');
|
|
291
486
|
return;
|
|
292
487
|
}
|
|
293
|
-
console.log(
|
|
294
|
-
// Recall relevant memories
|
|
488
|
+
console.log(`[Hindsight] Auto-recall for bank ${bankId}, prompt: ${prompt.substring(0, 50)}`);
|
|
489
|
+
// Recall relevant memories
|
|
295
490
|
const response = await client.recall({
|
|
296
491
|
query: prompt,
|
|
297
|
-
max_tokens:
|
|
492
|
+
max_tokens: 2048,
|
|
298
493
|
});
|
|
299
494
|
if (!response.results || response.results.length === 0) {
|
|
300
495
|
console.log('[Hindsight] No memories found for auto-recall');
|
|
@@ -308,7 +503,7 @@ ${memoriesJson}
|
|
|
308
503
|
|
|
309
504
|
User message: ${prompt}
|
|
310
505
|
</hindsight_memories>`;
|
|
311
|
-
console.log(`[Hindsight] Auto-recall: Injecting ${response.results.length} memories`);
|
|
506
|
+
console.log(`[Hindsight] Auto-recall: Injecting ${response.results.length} memories from bank ${bankId}`);
|
|
312
507
|
// Inject context before the user message
|
|
313
508
|
return { prependContext: contextMessage };
|
|
314
509
|
}
|
|
@@ -317,9 +512,14 @@ User message: ${prompt}
|
|
|
317
512
|
return;
|
|
318
513
|
}
|
|
319
514
|
});
|
|
320
|
-
|
|
515
|
+
// Hook signature: (event, ctx) where event has {messages, success, error?, durationMs?}
|
|
516
|
+
api.on('agent_end', async (event, ctx) => {
|
|
321
517
|
try {
|
|
322
|
-
|
|
518
|
+
// Use context from this hook, or fall back to context captured in before_agent_start
|
|
519
|
+
const effectiveCtx = ctx || currentAgentContext;
|
|
520
|
+
// Derive bank ID from context
|
|
521
|
+
const bankId = deriveBankId(effectiveCtx, pluginConfig);
|
|
522
|
+
console.log(`[Hindsight Hook] agent_end triggered - bank: ${bankId}`);
|
|
323
523
|
// Check event success and messages
|
|
324
524
|
if (!event.success || !Array.isArray(event.messages) || event.messages.length === 0) {
|
|
325
525
|
console.log('[Hindsight Hook] Skipping: success:', event.success, 'messages:', event.messages?.length);
|
|
@@ -332,7 +532,8 @@ User message: ${prompt}
|
|
|
332
532
|
return;
|
|
333
533
|
}
|
|
334
534
|
await clientGlobal.waitForReady();
|
|
335
|
-
|
|
535
|
+
// Get client configured for this context's bank (async to handle mission setup)
|
|
536
|
+
const client = await clientGlobal.getClientForContext(effectiveCtx);
|
|
336
537
|
if (!client) {
|
|
337
538
|
console.warn('[Hindsight] Client not initialized, skipping retain');
|
|
338
539
|
return;
|
|
@@ -359,8 +560,9 @@ User message: ${prompt}
|
|
|
359
560
|
console.log('[Hindsight Hook] Transcript too short, skipping');
|
|
360
561
|
return;
|
|
361
562
|
}
|
|
362
|
-
// Use
|
|
363
|
-
|
|
563
|
+
// Use unique document ID per conversation (sessionKey + timestamp)
|
|
564
|
+
// Static sessionKey (e.g. "agent:main:main") causes CASCADE delete of old memories
|
|
565
|
+
const documentId = `${effectiveCtx?.sessionKey || currentSessionKey || 'session'}-${Date.now()}`;
|
|
364
566
|
// Retain to Hindsight
|
|
365
567
|
await client.retain({
|
|
366
568
|
content: transcript,
|
|
@@ -368,15 +570,18 @@ User message: ${prompt}
|
|
|
368
570
|
metadata: {
|
|
369
571
|
retained_at: new Date().toISOString(),
|
|
370
572
|
message_count: event.messages.length,
|
|
573
|
+
channel_type: effectiveCtx?.messageProvider,
|
|
574
|
+
channel_id: effectiveCtx?.channelId,
|
|
575
|
+
sender_id: effectiveCtx?.senderId,
|
|
371
576
|
},
|
|
372
577
|
});
|
|
373
|
-
console.log(`[Hindsight] Retained ${event.messages.length} messages for session ${documentId}`);
|
|
578
|
+
console.log(`[Hindsight] Retained ${event.messages.length} messages to bank ${bankId} for session ${documentId}`);
|
|
374
579
|
}
|
|
375
580
|
catch (error) {
|
|
376
581
|
console.error('[Hindsight] Error retaining messages:', error);
|
|
377
582
|
}
|
|
378
583
|
});
|
|
379
|
-
console.log('[Hindsight]
|
|
584
|
+
console.log('[Hindsight] Hooks registered');
|
|
380
585
|
}
|
|
381
586
|
catch (error) {
|
|
382
587
|
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
|
}
|
|
@@ -34,6 +34,10 @@ export interface PluginConfig {
|
|
|
34
34
|
llmModel?: string;
|
|
35
35
|
llmApiKeyEnv?: string;
|
|
36
36
|
apiPort?: number;
|
|
37
|
+
hindsightApiUrl?: string;
|
|
38
|
+
hindsightApiToken?: string;
|
|
39
|
+
dynamicBankId?: boolean;
|
|
40
|
+
bankIdPrefix?: string;
|
|
37
41
|
}
|
|
38
42
|
export interface ServiceConfig {
|
|
39
43
|
id: string;
|
package/openclaw.plugin.json
CHANGED
|
@@ -46,6 +46,23 @@
|
|
|
46
46
|
"type": "number",
|
|
47
47
|
"description": "Port for the openclaw profile daemon (default: 9077)",
|
|
48
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
|
+
},
|
|
54
|
+
"hindsightApiToken": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "API token for external Hindsight API authentication. Required if the external API has authentication enabled."
|
|
57
|
+
},
|
|
58
|
+
"dynamicBankId": {
|
|
59
|
+
"type": "boolean",
|
|
60
|
+
"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.",
|
|
61
|
+
"default": true
|
|
62
|
+
},
|
|
63
|
+
"bankIdPrefix": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "Optional prefix for bank IDs (e.g., 'prod' results in 'prod-slack-C123'). Useful for separating environments."
|
|
49
66
|
}
|
|
50
67
|
},
|
|
51
68
|
"additionalProperties": false
|
|
@@ -86,6 +103,22 @@
|
|
|
86
103
|
"apiPort": {
|
|
87
104
|
"label": "API Port",
|
|
88
105
|
"placeholder": "9077 (default)"
|
|
106
|
+
},
|
|
107
|
+
"hindsightApiUrl": {
|
|
108
|
+
"label": "External Hindsight API URL",
|
|
109
|
+
"placeholder": "e.g. https://mcp.hindsight.devcraft.team (leave empty for local daemon)"
|
|
110
|
+
},
|
|
111
|
+
"hindsightApiToken": {
|
|
112
|
+
"label": "External API Token",
|
|
113
|
+
"placeholder": "API token if external API requires authentication"
|
|
114
|
+
},
|
|
115
|
+
"dynamicBankId": {
|
|
116
|
+
"label": "Dynamic Bank IDs",
|
|
117
|
+
"placeholder": "true (isolate memories per channel)"
|
|
118
|
+
},
|
|
119
|
+
"bankIdPrefix": {
|
|
120
|
+
"label": "Bank ID Prefix",
|
|
121
|
+
"placeholder": "e.g., prod, staging (optional)"
|
|
89
122
|
}
|
|
90
123
|
}
|
|
91
124
|
}
|
package/package.json
CHANGED