hindclaw-openclaw 0.1.0
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 +41 -0
- package/dist/client.js +211 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +132 -0
- package/dist/debug.d.ts +3 -0
- package/dist/debug.js +15 -0
- package/dist/derive-bank-id.d.ts +17 -0
- package/dist/derive-bank-id.js +60 -0
- package/dist/embed-manager.d.ts +32 -0
- package/dist/embed-manager.js +289 -0
- package/dist/format.d.ts +4 -0
- package/dist/format.js +20 -0
- package/dist/hooks/recall.d.ts +17 -0
- package/dist/hooks/recall.js +230 -0
- package/dist/hooks/retain.d.ts +26 -0
- package/dist/hooks/retain.js +178 -0
- package/dist/hooks/session-start.d.ts +3 -0
- package/dist/hooks/session-start.js +61 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +577 -0
- package/dist/moltbot-types.d.ts +27 -0
- package/dist/moltbot-types.js +3 -0
- package/dist/types.d.ts +256 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +51 -0
- package/dist/utils.js +230 -0
- package/openclaw.plugin.json +344 -0
- package/package.json +58 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { RetainRequest, RetainResponse, RecallRequest, RecallResponse, ReflectRequest, ReflectResponse, MentalModel, PluginHookAgentContext } from './types.js';
|
|
2
|
+
export declare function generateJwt(ctx: PluginHookAgentContext, jwtSecret: string, clientId: string): string;
|
|
3
|
+
export declare class HindsightHttpError extends Error {
|
|
4
|
+
status: number;
|
|
5
|
+
constructor(status: number, message: string);
|
|
6
|
+
}
|
|
7
|
+
export interface HindsightClientOptions {
|
|
8
|
+
llmModel?: string;
|
|
9
|
+
embedVersion?: string;
|
|
10
|
+
embedPackagePath?: string;
|
|
11
|
+
apiUrl?: string;
|
|
12
|
+
jwtSecret?: string;
|
|
13
|
+
clientId?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class HindsightClient {
|
|
16
|
+
private apiUrl?;
|
|
17
|
+
private jwtSecret?;
|
|
18
|
+
private clientId;
|
|
19
|
+
private llmModel?;
|
|
20
|
+
private embedVersion;
|
|
21
|
+
private embedPackagePath?;
|
|
22
|
+
constructor(opts: HindsightClientOptions);
|
|
23
|
+
get httpMode(): boolean;
|
|
24
|
+
retain(bankId: string, request: RetainRequest, ctx?: PluginHookAgentContext): Promise<RetainResponse>;
|
|
25
|
+
recall(bankId: string, request: RecallRequest, timeoutMs?: number, ctx?: PluginHookAgentContext): Promise<RecallResponse>;
|
|
26
|
+
reflect(bankId: string, request: ReflectRequest, ctx?: PluginHookAgentContext): Promise<ReflectResponse>;
|
|
27
|
+
getMentalModel(bankId: string, modelId: string, timeoutMs?: number, ctx?: PluginHookAgentContext): Promise<MentalModel>;
|
|
28
|
+
listMentalModels(bankId: string, ctx?: PluginHookAgentContext): Promise<MentalModel[]>;
|
|
29
|
+
private bankUrl;
|
|
30
|
+
private httpHeaders;
|
|
31
|
+
private requireHttpMode;
|
|
32
|
+
private httpRequest;
|
|
33
|
+
private httpRequestRaw;
|
|
34
|
+
/**
|
|
35
|
+
* Get the command and base args to run hindsight-embed.
|
|
36
|
+
* Returns [command, ...baseArgs] for use with execFile/spawn (no shell).
|
|
37
|
+
*/
|
|
38
|
+
private getEmbedCommand;
|
|
39
|
+
private retainSubprocess;
|
|
40
|
+
private recallSubprocess;
|
|
41
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { writeFile, mkdir, rm } from 'fs/promises';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { randomBytes, createHmac } from 'crypto';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const MAX_BUFFER = 5 * 1024 * 1024; // 5 MB — large transcripts can exceed default 1 MB
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
10
|
+
/** Strip null bytes from strings — Node 22 rejects them in execFile() args */
|
|
11
|
+
const sanitize = (s) => s.replace(/\0/g, '');
|
|
12
|
+
/**
|
|
13
|
+
* Sanitize a string for use as a cross-platform filename.
|
|
14
|
+
* Replaces characters illegal on Windows or Unix with underscores.
|
|
15
|
+
*/
|
|
16
|
+
function sanitizeFilename(name) {
|
|
17
|
+
return name.replace(/[\\/:*?"<>|\x00-\x1f]/g, '_').slice(0, 200) || 'content';
|
|
18
|
+
}
|
|
19
|
+
export function generateJwt(ctx, jwtSecret, clientId) {
|
|
20
|
+
const header = { alg: 'HS256', typ: 'JWT' };
|
|
21
|
+
const now = Math.floor(Date.now() / 1000);
|
|
22
|
+
const payload = {
|
|
23
|
+
client_id: clientId,
|
|
24
|
+
sender: ctx.senderId ? `${ctx.messageProvider}:${ctx.senderId}` : undefined,
|
|
25
|
+
agent: ctx.agentId,
|
|
26
|
+
channel: ctx.messageProvider,
|
|
27
|
+
topic: ctx.channelId || undefined,
|
|
28
|
+
iat: now,
|
|
29
|
+
exp: now + 300,
|
|
30
|
+
};
|
|
31
|
+
const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');
|
|
32
|
+
const unsigned = `${b64(header)}.${b64(payload)}`;
|
|
33
|
+
const signature = createHmac('sha256', jwtSecret).update(unsigned).digest('base64url');
|
|
34
|
+
return `${unsigned}.${signature}`;
|
|
35
|
+
}
|
|
36
|
+
export class HindsightHttpError extends Error {
|
|
37
|
+
status;
|
|
38
|
+
constructor(status, message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.status = status;
|
|
41
|
+
this.name = 'HindsightHttpError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class HindsightClient {
|
|
45
|
+
apiUrl;
|
|
46
|
+
jwtSecret;
|
|
47
|
+
clientId;
|
|
48
|
+
llmModel;
|
|
49
|
+
embedVersion;
|
|
50
|
+
embedPackagePath;
|
|
51
|
+
constructor(opts) {
|
|
52
|
+
this.llmModel = opts.llmModel;
|
|
53
|
+
this.embedVersion = opts.embedVersion || 'latest';
|
|
54
|
+
this.embedPackagePath = opts.embedPackagePath;
|
|
55
|
+
this.apiUrl = opts.apiUrl?.replace(/\/$/, '');
|
|
56
|
+
this.jwtSecret = opts.jwtSecret;
|
|
57
|
+
this.clientId = opts.clientId || 'openclaw';
|
|
58
|
+
}
|
|
59
|
+
get httpMode() {
|
|
60
|
+
return !!this.apiUrl;
|
|
61
|
+
}
|
|
62
|
+
// ── Core memory operations ───────────────────────────────────────
|
|
63
|
+
async retain(bankId, request, ctx) {
|
|
64
|
+
if (this.httpMode) {
|
|
65
|
+
return this.httpRequest('POST', `${this.bankUrl(bankId)}/memories`, {
|
|
66
|
+
items: request.items,
|
|
67
|
+
async: request.async ?? true,
|
|
68
|
+
}, undefined, ctx);
|
|
69
|
+
}
|
|
70
|
+
return this.retainSubprocess(bankId, request);
|
|
71
|
+
}
|
|
72
|
+
async recall(bankId, request, timeoutMs, ctx) {
|
|
73
|
+
if (this.httpMode) {
|
|
74
|
+
// Defense-in-depth: truncate query to stay under API's 500-token limit
|
|
75
|
+
const MAX_QUERY_CHARS = 800;
|
|
76
|
+
const query = request.query.length > MAX_QUERY_CHARS
|
|
77
|
+
? (console.warn(`[Hindsight] Truncating recall query from ${request.query.length} to ${MAX_QUERY_CHARS} chars`),
|
|
78
|
+
request.query.substring(0, MAX_QUERY_CHARS))
|
|
79
|
+
: request.query;
|
|
80
|
+
const body = {
|
|
81
|
+
query,
|
|
82
|
+
max_tokens: request.max_tokens || 1024,
|
|
83
|
+
};
|
|
84
|
+
if (request.budget) {
|
|
85
|
+
body.budget = request.budget;
|
|
86
|
+
}
|
|
87
|
+
if (request.types) {
|
|
88
|
+
body.types = request.types;
|
|
89
|
+
}
|
|
90
|
+
if (request.tag_groups) {
|
|
91
|
+
body.tag_groups = request.tag_groups;
|
|
92
|
+
}
|
|
93
|
+
return this.httpRequest('POST', `${this.bankUrl(bankId)}/memories/recall`, body, timeoutMs ?? DEFAULT_TIMEOUT_MS, ctx);
|
|
94
|
+
}
|
|
95
|
+
return this.recallSubprocess(bankId, request, timeoutMs);
|
|
96
|
+
}
|
|
97
|
+
async reflect(bankId, request, ctx) {
|
|
98
|
+
this.requireHttpMode('reflect');
|
|
99
|
+
return this.httpRequest('POST', `${this.bankUrl(bankId)}/reflect`, request, undefined, ctx);
|
|
100
|
+
}
|
|
101
|
+
// ── Mental models ────────────────────────────────────────────────
|
|
102
|
+
async getMentalModel(bankId, modelId, timeoutMs, ctx) {
|
|
103
|
+
this.requireHttpMode('getMentalModel');
|
|
104
|
+
return this.httpRequest('GET', `${this.bankUrl(bankId)}/mental-models/${encodeURIComponent(modelId)}`, undefined, timeoutMs, ctx);
|
|
105
|
+
}
|
|
106
|
+
async listMentalModels(bankId, ctx) {
|
|
107
|
+
this.requireHttpMode('listMentalModels');
|
|
108
|
+
const resp = await this.httpRequest('GET', `${this.bankUrl(bankId)}/mental-models`, undefined, undefined, ctx);
|
|
109
|
+
return resp.items;
|
|
110
|
+
}
|
|
111
|
+
// ── Internal: URL + HTTP helpers ─────────────────────────────────
|
|
112
|
+
bankUrl(bankId) {
|
|
113
|
+
return `${this.apiUrl}/v1/default/banks/${encodeURIComponent(bankId)}`;
|
|
114
|
+
}
|
|
115
|
+
httpHeaders(ctx) {
|
|
116
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
117
|
+
if (this.jwtSecret && ctx) {
|
|
118
|
+
headers['Authorization'] = `Bearer ${generateJwt(ctx, this.jwtSecret, this.clientId)}`;
|
|
119
|
+
}
|
|
120
|
+
return headers;
|
|
121
|
+
}
|
|
122
|
+
requireHttpMode(method) {
|
|
123
|
+
if (!this.httpMode) {
|
|
124
|
+
throw new Error(`${method} requires HTTP mode (apiUrl must be set)`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async httpRequest(method, url, body, timeoutMs, ctx) {
|
|
128
|
+
const res = await this.httpRequestRaw(method, url, body, timeoutMs, ctx);
|
|
129
|
+
// For 204 No Content, return undefined cast as T
|
|
130
|
+
if (res.status === 204) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return res.json();
|
|
134
|
+
}
|
|
135
|
+
async httpRequestRaw(method, url, body, timeoutMs, ctx) {
|
|
136
|
+
const opts = {
|
|
137
|
+
method,
|
|
138
|
+
headers: this.httpHeaders(ctx),
|
|
139
|
+
signal: AbortSignal.timeout(timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
140
|
+
};
|
|
141
|
+
if (body !== undefined) {
|
|
142
|
+
opts.body = JSON.stringify(body);
|
|
143
|
+
}
|
|
144
|
+
const res = await fetch(url, opts);
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
const text = await res.text().catch(() => '');
|
|
147
|
+
throw new HindsightHttpError(res.status, `HTTP ${res.status}: ${text}`);
|
|
148
|
+
}
|
|
149
|
+
return res;
|
|
150
|
+
}
|
|
151
|
+
// ── Internal: subprocess mode (retain/recall only) ───────────────
|
|
152
|
+
/**
|
|
153
|
+
* Get the command and base args to run hindsight-embed.
|
|
154
|
+
* Returns [command, ...baseArgs] for use with execFile/spawn (no shell).
|
|
155
|
+
*/
|
|
156
|
+
getEmbedCommand() {
|
|
157
|
+
if (this.embedPackagePath) {
|
|
158
|
+
return ['uv', 'run', '--directory', this.embedPackagePath, 'hindsight-embed'];
|
|
159
|
+
}
|
|
160
|
+
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
|
|
161
|
+
// Inject claude-agent-sdk when using claude-code provider (uvx runs in isolated venv)
|
|
162
|
+
const provider = process.env.HINDSIGHT_API_LLM_PROVIDER;
|
|
163
|
+
if (provider === 'claude-code') {
|
|
164
|
+
return ['uvx', '--with', 'claude-agent-sdk', embedPackage];
|
|
165
|
+
}
|
|
166
|
+
return ['uvx', embedPackage];
|
|
167
|
+
}
|
|
168
|
+
async retainSubprocess(bankId, request) {
|
|
169
|
+
// Subprocess mode: use first item's content, write to temp file
|
|
170
|
+
const content = request.items.map(i => i.content).join('\n\n');
|
|
171
|
+
const docId = request.items[0]?.document_id || 'conversation';
|
|
172
|
+
const tempDir = join(tmpdir(), `hindsight_${randomBytes(8).toString('hex')}`);
|
|
173
|
+
const safeFilename = sanitizeFilename(docId);
|
|
174
|
+
const tempFile = join(tempDir, `${safeFilename}.txt`);
|
|
175
|
+
try {
|
|
176
|
+
await mkdir(tempDir, { recursive: true });
|
|
177
|
+
await writeFile(tempFile, sanitize(content), 'utf8');
|
|
178
|
+
const [cmd, ...baseArgs] = this.getEmbedCommand();
|
|
179
|
+
const args = [...baseArgs, '--profile', 'openclaw', 'memory', 'retain-files', bankId, tempFile, '--async'];
|
|
180
|
+
const { stdout } = await execFileAsync(cmd, args, { maxBuffer: MAX_BUFFER });
|
|
181
|
+
console.log(`[Hindsight] Retained (async): ${stdout.trim()}`);
|
|
182
|
+
return {
|
|
183
|
+
message: 'Memory queued for background processing',
|
|
184
|
+
document_id: docId,
|
|
185
|
+
memory_unit_ids: [],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
throw new Error(`Failed to retain memory: ${error}`, { cause: error });
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async recallSubprocess(bankId, request, timeoutMs) {
|
|
196
|
+
const query = sanitize(request.query);
|
|
197
|
+
const maxTokens = request.max_tokens || 1024;
|
|
198
|
+
const [cmd, ...baseArgs] = this.getEmbedCommand();
|
|
199
|
+
const args = [...baseArgs, '--profile', 'openclaw', 'memory', 'recall', bankId, query, '--output', 'json', '--max-tokens', String(maxTokens)];
|
|
200
|
+
try {
|
|
201
|
+
const { stdout } = await execFileAsync(cmd, args, {
|
|
202
|
+
maxBuffer: MAX_BUFFER,
|
|
203
|
+
timeout: timeoutMs ?? 30_000,
|
|
204
|
+
});
|
|
205
|
+
return JSON.parse(stdout);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
throw new Error(`Failed to recall memories: ${error}`, { cause: error });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BankConfig, PluginConfig, ResolvedConfig, AgentEntry } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Recursively resolve $include directives in a parsed config object.
|
|
4
|
+
* Each { "$include": "./path" } is replaced with the parsed contents of the referenced file.
|
|
5
|
+
* Paths are resolved relative to basePath.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveIncludes<T>(obj: T, basePath: string, depth?: number, seen?: Set<string>): T;
|
|
8
|
+
/**
|
|
9
|
+
* Parse a JSON5 bank config file content into a BankConfig object.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseBankConfigFile(content: string): BankConfig;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve per-agent config by merging plugin defaults with bank config overrides.
|
|
14
|
+
*
|
|
15
|
+
* Resolution order: pluginDefaults → bankFile (shallow merge, bank file wins).
|
|
16
|
+
*
|
|
17
|
+
* Extracted behavioral fields (recallFrom, sessionStartModels, reflect*) are
|
|
18
|
+
* hoisted to their underscore-prefixed counterparts.
|
|
19
|
+
* Everything else is merged as behavioral/infrastructure overrides.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveAgentConfig(agentId: string, pluginDefaults: Omit<PluginConfig, 'agents'>, bankConfigs: Map<string, BankConfig>): ResolvedConfig;
|
|
22
|
+
/**
|
|
23
|
+
* Load all bank config files for the given agents map.
|
|
24
|
+
* Reads files synchronously — called at plugin init time, not in the hot path.
|
|
25
|
+
*
|
|
26
|
+
* @param agents Record<agentId, AgentEntry> from PluginConfig.agents
|
|
27
|
+
* @param basePath Base directory to resolve relative bankConfig paths against
|
|
28
|
+
* @returns Map<agentId, BankConfig>
|
|
29
|
+
*/
|
|
30
|
+
export declare function loadBankConfigFiles(agents: Record<string, AgentEntry>, basePath: string): Map<string, BankConfig>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import JSON5 from 'json5';
|
|
4
|
+
import { debug } from './debug.js';
|
|
5
|
+
// ── Field classification sets ─────────────────────────────────────────
|
|
6
|
+
const EXTRACTED_FIELDS = new Set([
|
|
7
|
+
'recallFrom',
|
|
8
|
+
'sessionStartModels',
|
|
9
|
+
'reflectOnRecall',
|
|
10
|
+
'reflectBudget',
|
|
11
|
+
'reflectMaxTokens',
|
|
12
|
+
]);
|
|
13
|
+
// ── $include resolution ───────────────────────────────────────────────
|
|
14
|
+
const MAX_INCLUDE_DEPTH = 10;
|
|
15
|
+
/**
|
|
16
|
+
* Recursively resolve $include directives in a parsed config object.
|
|
17
|
+
* Each { "$include": "./path" } is replaced with the parsed contents of the referenced file.
|
|
18
|
+
* Paths are resolved relative to basePath.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveIncludes(obj, basePath, depth = 0, seen = new Set()) {
|
|
21
|
+
if (depth > MAX_INCLUDE_DEPTH) {
|
|
22
|
+
throw new Error(`$include depth exceeded (max ${MAX_INCLUDE_DEPTH})`);
|
|
23
|
+
}
|
|
24
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
25
|
+
return obj;
|
|
26
|
+
}
|
|
27
|
+
const record = obj;
|
|
28
|
+
// Check if this object IS an $include directive
|
|
29
|
+
if ('$include' in record && typeof record.$include === 'string' && Object.keys(record).length === 1) {
|
|
30
|
+
const includePath = record.$include;
|
|
31
|
+
const filePath = includePath.startsWith('/') ? includePath : join(basePath, includePath);
|
|
32
|
+
if (seen.has(filePath)) {
|
|
33
|
+
throw new Error(`Circular $include detected: ${filePath}`);
|
|
34
|
+
}
|
|
35
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
36
|
+
const parsed = JSON5.parse(content);
|
|
37
|
+
const nextSeen = new Set(seen);
|
|
38
|
+
nextSeen.add(filePath);
|
|
39
|
+
return resolveIncludes(parsed, dirname(filePath), depth + 1, nextSeen);
|
|
40
|
+
}
|
|
41
|
+
// Walk child properties
|
|
42
|
+
const result = {};
|
|
43
|
+
for (const [key, value] of Object.entries(record)) {
|
|
44
|
+
result[key] = resolveIncludes(value, basePath, depth, seen);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
// ── Public API ────────────────────────────────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* Parse a JSON5 bank config file content into a BankConfig object.
|
|
51
|
+
*/
|
|
52
|
+
export function parseBankConfigFile(content) {
|
|
53
|
+
return JSON5.parse(content);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve per-agent config by merging plugin defaults with bank config overrides.
|
|
57
|
+
*
|
|
58
|
+
* Resolution order: pluginDefaults → bankFile (shallow merge, bank file wins).
|
|
59
|
+
*
|
|
60
|
+
* Extracted behavioral fields (recallFrom, sessionStartModels, reflect*) are
|
|
61
|
+
* hoisted to their underscore-prefixed counterparts.
|
|
62
|
+
* Everything else is merged as behavioral/infrastructure overrides.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveAgentConfig(agentId, pluginDefaults, bankConfigs) {
|
|
65
|
+
const bankConfig = bankConfigs.get(agentId);
|
|
66
|
+
if (!bankConfig) {
|
|
67
|
+
debug(`[Hindsight] No bank config for agent "${agentId}" — using plugin defaults`);
|
|
68
|
+
return {
|
|
69
|
+
...pluginDefaults,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Separate extracted fields from behavioral overrides
|
|
73
|
+
const overrides = {};
|
|
74
|
+
for (const [key, value] of Object.entries(bankConfig)) {
|
|
75
|
+
if (!EXTRACTED_FIELDS.has(key)) {
|
|
76
|
+
overrides[key] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Build the merged behavioral/infrastructure config
|
|
80
|
+
const merged = {
|
|
81
|
+
...pluginDefaults,
|
|
82
|
+
...overrides,
|
|
83
|
+
};
|
|
84
|
+
// Hoist extracted fields
|
|
85
|
+
if (bankConfig.recallFrom !== undefined) {
|
|
86
|
+
merged._recallFrom = bankConfig.recallFrom;
|
|
87
|
+
}
|
|
88
|
+
if (bankConfig.sessionStartModels !== undefined) {
|
|
89
|
+
merged._sessionStartModels = bankConfig.sessionStartModels;
|
|
90
|
+
}
|
|
91
|
+
if (bankConfig.reflectOnRecall !== undefined) {
|
|
92
|
+
merged._reflectOnRecall = bankConfig.reflectOnRecall;
|
|
93
|
+
}
|
|
94
|
+
if (bankConfig.reflectBudget !== undefined) {
|
|
95
|
+
merged._reflectBudget = bankConfig.reflectBudget;
|
|
96
|
+
}
|
|
97
|
+
if (bankConfig.reflectMaxTokens !== undefined) {
|
|
98
|
+
merged._reflectMaxTokens = bankConfig.reflectMaxTokens;
|
|
99
|
+
}
|
|
100
|
+
return merged;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Load all bank config files for the given agents map.
|
|
104
|
+
* Reads files synchronously — called at plugin init time, not in the hot path.
|
|
105
|
+
*
|
|
106
|
+
* @param agents Record<agentId, AgentEntry> from PluginConfig.agents
|
|
107
|
+
* @param basePath Base directory to resolve relative bankConfig paths against
|
|
108
|
+
* @returns Map<agentId, BankConfig>
|
|
109
|
+
*/
|
|
110
|
+
export function loadBankConfigFiles(agents, basePath) {
|
|
111
|
+
const result = new Map();
|
|
112
|
+
for (const [agentId, entry] of Object.entries(agents)) {
|
|
113
|
+
if (!entry?.bankConfig) {
|
|
114
|
+
console.warn(`[Hindsight] Agent "${agentId}" has no bankConfig path — skipping`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const filePath = entry.bankConfig.startsWith('/')
|
|
118
|
+
? entry.bankConfig
|
|
119
|
+
: join(basePath, entry.bankConfig);
|
|
120
|
+
try {
|
|
121
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
122
|
+
const parsed = parseBankConfigFile(content);
|
|
123
|
+
const resolved = resolveIncludes(parsed, dirname(filePath));
|
|
124
|
+
result.set(agentId, resolved);
|
|
125
|
+
debug(`[Hindsight] Loaded bank config for agent "${agentId}" from ${filePath}`);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.warn(`[Hindsight] Failed to load bank config for agent "${agentId}" from ${filePath}:`, error instanceof Error ? error.message : error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
package/dist/debug.d.ts
ADDED
package/dist/debug.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// ── Shared debug logging ─────────────────────────────────────────────
|
|
2
|
+
// Silent by default, enable with debug: true in plugin config.
|
|
3
|
+
// Extracted into a standalone module so hooks can import without
|
|
4
|
+
// circular dependencies back to index.ts.
|
|
5
|
+
let debugEnabled = false;
|
|
6
|
+
export const debug = (...args) => {
|
|
7
|
+
if (debugEnabled)
|
|
8
|
+
console.log(...args);
|
|
9
|
+
};
|
|
10
|
+
export function setDebugEnabled(value) {
|
|
11
|
+
debugEnabled = value;
|
|
12
|
+
}
|
|
13
|
+
export function isDebugEnabled() {
|
|
14
|
+
return debugEnabled;
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PluginHookAgentContext, PluginConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse the OpenClaw sessionKey to extract context fields.
|
|
4
|
+
* Format: "agent:{agentId}:{provider}:{channelType}:{channelId}[:{extra}]"
|
|
5
|
+
* Example: "agent:c0der:telegram:group:-1003825475854:topic:42"
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseSessionKey(sessionKey: string): {
|
|
8
|
+
agentId?: string;
|
|
9
|
+
provider?: string;
|
|
10
|
+
channel?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Derive a bank ID from the agent context.
|
|
14
|
+
* Uses configurable dynamicBankGranularity to determine bank segmentation.
|
|
15
|
+
* Falls back to default bank when context is unavailable.
|
|
16
|
+
*/
|
|
17
|
+
export declare function deriveBankId(ctx: PluginHookAgentContext | undefined, pluginConfig: PluginConfig): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { debug } from './debug.js';
|
|
2
|
+
// Default bank name (fallback when channel context not available)
|
|
3
|
+
const DEFAULT_BANK_NAME = 'openclaw';
|
|
4
|
+
/**
|
|
5
|
+
* Parse the OpenClaw sessionKey to extract context fields.
|
|
6
|
+
* Format: "agent:{agentId}:{provider}:{channelType}:{channelId}[:{extra}]"
|
|
7
|
+
* Example: "agent:c0der:telegram:group:-1003825475854:topic:42"
|
|
8
|
+
*/
|
|
9
|
+
export function parseSessionKey(sessionKey) {
|
|
10
|
+
const parts = sessionKey.split(':');
|
|
11
|
+
if (parts.length < 5 || parts[0] !== 'agent')
|
|
12
|
+
return {};
|
|
13
|
+
// parts[1] = agentId, parts[2] = provider, parts[3] = channelType, parts[4..] = channelId + extras
|
|
14
|
+
return {
|
|
15
|
+
agentId: parts[1],
|
|
16
|
+
provider: parts[2],
|
|
17
|
+
// Rejoin from channelType onward as the channel identifier (e.g. "group:-1003825475854:topic:42")
|
|
18
|
+
channel: parts.slice(3).join(':'),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Derive a bank ID from the agent context.
|
|
23
|
+
* Uses configurable dynamicBankGranularity to determine bank segmentation.
|
|
24
|
+
* Falls back to default bank when context is unavailable.
|
|
25
|
+
*/
|
|
26
|
+
export function deriveBankId(ctx, pluginConfig) {
|
|
27
|
+
if (pluginConfig.dynamicBankId === false) {
|
|
28
|
+
return pluginConfig.bankIdPrefix ? `${pluginConfig.bankIdPrefix}-${DEFAULT_BANK_NAME}` : DEFAULT_BANK_NAME;
|
|
29
|
+
}
|
|
30
|
+
// When no context is available, fall back to the static default bank.
|
|
31
|
+
if (!ctx) {
|
|
32
|
+
return pluginConfig.bankIdPrefix ? `${pluginConfig.bankIdPrefix}-${DEFAULT_BANK_NAME}` : DEFAULT_BANK_NAME;
|
|
33
|
+
}
|
|
34
|
+
const fields = pluginConfig.dynamicBankGranularity?.length ? pluginConfig.dynamicBankGranularity : ['agent', 'channel', 'user'];
|
|
35
|
+
// Validate field names at runtime — typos silently produce 'unknown' segments
|
|
36
|
+
const validFields = new Set(['agent', 'channel', 'user', 'provider']);
|
|
37
|
+
for (const f of fields) {
|
|
38
|
+
if (!validFields.has(f)) {
|
|
39
|
+
console.warn(`[Hindsight] Unknown dynamicBankGranularity field "${f}" — will resolve to "unknown" in bank ID. Valid fields: agent, channel, user, provider`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Parse sessionKey as fallback when direct context fields are missing
|
|
43
|
+
const sessionParsed = ctx?.sessionKey ? parseSessionKey(ctx.sessionKey) : {};
|
|
44
|
+
// Warn when 'user' is in active fields but senderId is missing
|
|
45
|
+
if (fields.includes('user') && ctx && !ctx.senderId) {
|
|
46
|
+
debug('[Hindsight] senderId not available in context — bank ID will use "anonymous". Ensure your OpenClaw provider passes senderId.');
|
|
47
|
+
}
|
|
48
|
+
const fieldMap = {
|
|
49
|
+
agent: ctx?.agentId || sessionParsed.agentId || 'default',
|
|
50
|
+
channel: ctx?.channelId || sessionParsed.channel || 'unknown',
|
|
51
|
+
user: ctx?.senderId || 'anonymous',
|
|
52
|
+
provider: ctx?.messageProvider || sessionParsed.provider || 'unknown',
|
|
53
|
+
};
|
|
54
|
+
const baseBankId = fields
|
|
55
|
+
.map(f => encodeURIComponent(fieldMap[f] || 'unknown'))
|
|
56
|
+
.join('::');
|
|
57
|
+
return pluginConfig.bankIdPrefix
|
|
58
|
+
? `${pluginConfig.bankIdPrefix}-${baseBankId}`
|
|
59
|
+
: baseBankId;
|
|
60
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare class HindsightEmbedManager {
|
|
2
|
+
private process;
|
|
3
|
+
private port;
|
|
4
|
+
private baseUrl;
|
|
5
|
+
private embedDir;
|
|
6
|
+
private llmProvider;
|
|
7
|
+
private llmApiKey;
|
|
8
|
+
private llmModel?;
|
|
9
|
+
private llmBaseUrl?;
|
|
10
|
+
private daemonIdleTimeout;
|
|
11
|
+
private embedVersion;
|
|
12
|
+
private embedPackagePath?;
|
|
13
|
+
private jwtSecret?;
|
|
14
|
+
private clientId;
|
|
15
|
+
constructor(port: number, llmProvider: string, llmApiKey: string, llmModel?: string, llmBaseUrl?: string, daemonIdleTimeout?: number, // Default: never timeout
|
|
16
|
+
embedVersion?: string, // Default: latest
|
|
17
|
+
embedPackagePath?: string, // Local path to hindsight package
|
|
18
|
+
jwtSecret?: string, clientId?: string);
|
|
19
|
+
/**
|
|
20
|
+
* Get the command to run hindsight-embed (either local or from PyPI)
|
|
21
|
+
*/
|
|
22
|
+
private getEmbedCommand;
|
|
23
|
+
private getExtraEmbedPackages;
|
|
24
|
+
start(): Promise<void>;
|
|
25
|
+
stop(): Promise<void>;
|
|
26
|
+
private waitForReady;
|
|
27
|
+
getBaseUrl(): string;
|
|
28
|
+
isRunning(): boolean;
|
|
29
|
+
checkHealth(): Promise<boolean>;
|
|
30
|
+
private configureProfile;
|
|
31
|
+
private resolvePg0Port;
|
|
32
|
+
}
|