@vectorize-io/hindsight-openclaw 0.4.11 → 0.4.12
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 +24 -27
- package/dist/client.js +161 -73
- package/dist/index.d.ts +15 -0
- package/dist/index.js +204 -63
- package/dist/types.d.ts +15 -7
- package/openclaw.plugin.json +2 -2
- package/package.json +4 -3
package/dist/client.d.ts
CHANGED
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
import type { RetainRequest, RetainResponse, RecallRequest, RecallResponse } from './types.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
export interface HindsightClientOptions {
|
|
3
|
+
llmProvider: string;
|
|
4
|
+
llmApiKey: string;
|
|
5
|
+
llmModel?: string;
|
|
6
|
+
embedVersion?: string;
|
|
7
|
+
embedPackagePath?: string;
|
|
8
|
+
apiUrl?: string;
|
|
9
|
+
apiToken?: string;
|
|
10
|
+
}
|
|
25
11
|
export declare class HindsightClient {
|
|
26
12
|
private bankId;
|
|
27
13
|
private llmProvider;
|
|
@@ -29,13 +15,24 @@ export declare class HindsightClient {
|
|
|
29
15
|
private llmModel?;
|
|
30
16
|
private embedVersion;
|
|
31
17
|
private embedPackagePath?;
|
|
32
|
-
|
|
18
|
+
private apiUrl?;
|
|
19
|
+
private apiToken?;
|
|
20
|
+
constructor(opts: HindsightClientOptions);
|
|
21
|
+
private get httpMode();
|
|
33
22
|
/**
|
|
34
|
-
* Get the command
|
|
23
|
+
* Get the command and base args to run hindsight-embed.
|
|
24
|
+
* Returns [command, ...baseArgs] for use with execFile/spawn (no shell).
|
|
35
25
|
*/
|
|
36
|
-
private
|
|
26
|
+
private getEmbedCommand;
|
|
27
|
+
private httpHeaders;
|
|
37
28
|
setBankId(bankId: string): void;
|
|
38
29
|
setBankMission(mission: string): Promise<void>;
|
|
30
|
+
private setBankMissionHttp;
|
|
31
|
+
private setBankMissionSubprocess;
|
|
39
32
|
retain(request: RetainRequest): Promise<RetainResponse>;
|
|
40
|
-
|
|
33
|
+
private retainHttp;
|
|
34
|
+
private retainSubprocess;
|
|
35
|
+
recall(request: RecallRequest, timeoutMs?: number): Promise<RecallResponse>;
|
|
36
|
+
private recallHttp;
|
|
37
|
+
private recallSubprocess;
|
|
41
38
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,74 +1,98 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
|
-
|
|
3
|
+
import { writeFile, mkdir, rm } from 'fs/promises';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { randomBytes } 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, '');
|
|
4
12
|
/**
|
|
5
|
-
*
|
|
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)
|
|
13
|
+
* Sanitize a string for use as a cross-platform filename.
|
|
14
|
+
* Replaces characters illegal on Windows or Unix with underscores.
|
|
25
15
|
*/
|
|
26
|
-
|
|
27
|
-
// Replace
|
|
28
|
-
|
|
29
|
-
// and starts a new single-quoted string.
|
|
30
|
-
return arg.replace(/'/g, "'\\''");
|
|
16
|
+
function sanitizeFilename(name) {
|
|
17
|
+
// Replace characters illegal on Windows (\/:*?"<>|) and control chars
|
|
18
|
+
return name.replace(/[\\/:*?"<>|\x00-\x1f]/g, '_').slice(0, 200) || 'content';
|
|
31
19
|
}
|
|
32
20
|
export class HindsightClient {
|
|
33
|
-
bankId = 'default';
|
|
21
|
+
bankId = 'default';
|
|
34
22
|
llmProvider;
|
|
35
23
|
llmApiKey;
|
|
36
24
|
llmModel;
|
|
37
25
|
embedVersion;
|
|
38
26
|
embedPackagePath;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.
|
|
43
|
-
this.
|
|
44
|
-
this.
|
|
27
|
+
apiUrl;
|
|
28
|
+
apiToken;
|
|
29
|
+
constructor(opts) {
|
|
30
|
+
this.llmProvider = opts.llmProvider;
|
|
31
|
+
this.llmApiKey = opts.llmApiKey;
|
|
32
|
+
this.llmModel = opts.llmModel;
|
|
33
|
+
this.embedVersion = opts.embedVersion || 'latest';
|
|
34
|
+
this.embedPackagePath = opts.embedPackagePath;
|
|
35
|
+
this.apiUrl = opts.apiUrl?.replace(/\/$/, ''); // strip trailing slash
|
|
36
|
+
this.apiToken = opts.apiToken;
|
|
37
|
+
}
|
|
38
|
+
get httpMode() {
|
|
39
|
+
return !!this.apiUrl;
|
|
45
40
|
}
|
|
46
41
|
/**
|
|
47
|
-
* Get the command
|
|
42
|
+
* Get the command and base args to run hindsight-embed.
|
|
43
|
+
* Returns [command, ...baseArgs] for use with execFile/spawn (no shell).
|
|
48
44
|
*/
|
|
49
|
-
|
|
45
|
+
getEmbedCommand() {
|
|
50
46
|
if (this.embedPackagePath) {
|
|
51
|
-
|
|
52
|
-
return `uv run --directory ${this.embedPackagePath} hindsight-embed`;
|
|
47
|
+
return ['uv', 'run', '--directory', this.embedPackagePath, 'hindsight-embed'];
|
|
53
48
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
|
|
50
|
+
return ['uvx', embedPackage];
|
|
51
|
+
}
|
|
52
|
+
httpHeaders() {
|
|
53
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
54
|
+
if (this.apiToken) {
|
|
55
|
+
headers['Authorization'] = `Bearer ${this.apiToken}`;
|
|
58
56
|
}
|
|
57
|
+
return headers;
|
|
59
58
|
}
|
|
60
59
|
setBankId(bankId) {
|
|
61
60
|
this.bankId = bankId;
|
|
62
61
|
}
|
|
62
|
+
// --- setBankMission ---
|
|
63
63
|
async setBankMission(mission) {
|
|
64
64
|
if (!mission || mission.trim().length === 0) {
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
if (this.httpMode) {
|
|
68
|
+
return this.setBankMissionHttp(mission);
|
|
69
|
+
}
|
|
70
|
+
return this.setBankMissionSubprocess(mission);
|
|
71
|
+
}
|
|
72
|
+
async setBankMissionHttp(mission) {
|
|
70
73
|
try {
|
|
71
|
-
const
|
|
74
|
+
const url = `${this.apiUrl}/v1/default/banks/${encodeURIComponent(this.bankId)}`;
|
|
75
|
+
const res = await fetch(url, {
|
|
76
|
+
method: 'PUT',
|
|
77
|
+
headers: this.httpHeaders(),
|
|
78
|
+
body: JSON.stringify({ mission }),
|
|
79
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const body = await res.text().catch(() => '');
|
|
83
|
+
throw new Error(`HTTP ${res.status}: ${body}`);
|
|
84
|
+
}
|
|
85
|
+
console.log(`[Hindsight] Bank mission set via HTTP`);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.warn(`[Hindsight] Could not set bank mission (bank may not exist yet): ${error}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async setBankMissionSubprocess(mission) {
|
|
92
|
+
const [cmd, ...baseArgs] = this.getEmbedCommand();
|
|
93
|
+
const args = [...baseArgs, '--profile', 'openclaw', 'bank', 'mission', this.bankId, sanitize(mission)];
|
|
94
|
+
try {
|
|
95
|
+
const { stdout } = await execFileAsync(cmd, args, { maxBuffer: MAX_BUFFER });
|
|
72
96
|
console.log(`[Hindsight] Bank mission set: ${stdout.trim()}`);
|
|
73
97
|
}
|
|
74
98
|
catch (error) {
|
|
@@ -76,15 +100,55 @@ export class HindsightClient {
|
|
|
76
100
|
console.warn(`[Hindsight] Could not set bank mission (bank may not exist yet): ${error}`);
|
|
77
101
|
}
|
|
78
102
|
}
|
|
103
|
+
// --- retain ---
|
|
79
104
|
async retain(request) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
105
|
+
if (this.httpMode) {
|
|
106
|
+
return this.retainHttp(request);
|
|
107
|
+
}
|
|
108
|
+
return this.retainSubprocess(request);
|
|
109
|
+
}
|
|
110
|
+
async retainHttp(request) {
|
|
111
|
+
const url = `${this.apiUrl}/v1/default/banks/${encodeURIComponent(this.bankId)}/memories`;
|
|
112
|
+
const body = {
|
|
113
|
+
items: [{
|
|
114
|
+
content: request.content,
|
|
115
|
+
document_id: request.document_id || 'conversation',
|
|
116
|
+
metadata: request.metadata,
|
|
117
|
+
}],
|
|
118
|
+
async: true,
|
|
119
|
+
};
|
|
120
|
+
const res = await fetch(url, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: this.httpHeaders(),
|
|
123
|
+
body: JSON.stringify(body),
|
|
124
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
|
|
125
|
+
});
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
const text = await res.text().catch(() => '');
|
|
128
|
+
throw new Error(`Failed to retain memory (HTTP ${res.status}): ${text}`);
|
|
129
|
+
}
|
|
130
|
+
const data = await res.json();
|
|
131
|
+
console.log(`[Hindsight] Retained via HTTP (async): ${JSON.stringify(data).substring(0, 200)}`);
|
|
132
|
+
return {
|
|
133
|
+
message: 'Memory queued for background processing',
|
|
134
|
+
document_id: request.document_id || 'conversation',
|
|
135
|
+
memory_unit_ids: [],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async retainSubprocess(request) {
|
|
139
|
+
const docId = request.document_id || 'conversation';
|
|
140
|
+
// Write content to a temp file to avoid E2BIG (ARG_MAX) errors when passing
|
|
141
|
+
// large conversations as arguments.
|
|
142
|
+
const tempDir = join(tmpdir(), `hindsight_${randomBytes(8).toString('hex')}`);
|
|
143
|
+
const safeFilename = sanitizeFilename(docId);
|
|
144
|
+
const tempFile = join(tempDir, `${safeFilename}.txt`);
|
|
84
145
|
try {
|
|
85
|
-
|
|
146
|
+
await mkdir(tempDir, { recursive: true });
|
|
147
|
+
await writeFile(tempFile, sanitize(request.content), 'utf8');
|
|
148
|
+
const [cmd, ...baseArgs] = this.getEmbedCommand();
|
|
149
|
+
const args = [...baseArgs, '--profile', 'openclaw', 'memory', 'retain-files', this.bankId, tempFile, '--async'];
|
|
150
|
+
const { stdout } = await execFileAsync(cmd, args, { maxBuffer: MAX_BUFFER });
|
|
86
151
|
console.log(`[Hindsight] Retained (async): ${stdout.trim()}`);
|
|
87
|
-
// Return a simple response
|
|
88
152
|
return {
|
|
89
153
|
message: 'Memory queued for background processing',
|
|
90
154
|
document_id: docId,
|
|
@@ -92,33 +156,57 @@ export class HindsightClient {
|
|
|
92
156
|
};
|
|
93
157
|
}
|
|
94
158
|
catch (error) {
|
|
95
|
-
throw new Error(`Failed to retain memory: ${error}
|
|
159
|
+
throw new Error(`Failed to retain memory: ${error}`, { cause: error });
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// --- recall ---
|
|
166
|
+
async recall(request, timeoutMs) {
|
|
167
|
+
if (this.httpMode) {
|
|
168
|
+
return this.recallHttp(request, timeoutMs);
|
|
169
|
+
}
|
|
170
|
+
return this.recallSubprocess(request, timeoutMs);
|
|
171
|
+
}
|
|
172
|
+
async recallHttp(request, timeoutMs) {
|
|
173
|
+
const url = `${this.apiUrl}/v1/default/banks/${encodeURIComponent(this.bankId)}/memories/recall`;
|
|
174
|
+
// Defense-in-depth: truncate query to stay under API's 500-token limit
|
|
175
|
+
const MAX_QUERY_CHARS = 800;
|
|
176
|
+
const query = request.query.length > MAX_QUERY_CHARS
|
|
177
|
+
? (console.warn(`[Hindsight] Truncating recall query from ${request.query.length} to ${MAX_QUERY_CHARS} chars`),
|
|
178
|
+
request.query.substring(0, MAX_QUERY_CHARS))
|
|
179
|
+
: request.query;
|
|
180
|
+
const body = {
|
|
181
|
+
query,
|
|
182
|
+
max_tokens: request.max_tokens || 1024,
|
|
183
|
+
};
|
|
184
|
+
const res = await fetch(url, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers: this.httpHeaders(),
|
|
187
|
+
body: JSON.stringify(body),
|
|
188
|
+
signal: AbortSignal.timeout(timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
189
|
+
});
|
|
190
|
+
if (!res.ok) {
|
|
191
|
+
const text = await res.text().catch(() => '');
|
|
192
|
+
throw new Error(`Failed to recall memories (HTTP ${res.status}): ${text}`);
|
|
96
193
|
}
|
|
194
|
+
return res.json();
|
|
97
195
|
}
|
|
98
|
-
async
|
|
99
|
-
const query =
|
|
196
|
+
async recallSubprocess(request, timeoutMs) {
|
|
197
|
+
const query = sanitize(request.query);
|
|
100
198
|
const maxTokens = request.max_tokens || 1024;
|
|
101
|
-
const
|
|
102
|
-
const
|
|
199
|
+
const [cmd, ...baseArgs] = this.getEmbedCommand();
|
|
200
|
+
const args = [...baseArgs, '--profile', 'openclaw', 'memory', 'recall', this.bankId, query, '--output', 'json', '--max-tokens', String(maxTokens)];
|
|
103
201
|
try {
|
|
104
|
-
const { stdout } = await
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return
|
|
109
|
-
results: results.map((r) => ({
|
|
110
|
-
content: r.text || r.content || '',
|
|
111
|
-
score: 1.0, // CLI doesn't return scores
|
|
112
|
-
metadata: {
|
|
113
|
-
document_id: r.document_id,
|
|
114
|
-
chunk_id: r.chunk_id,
|
|
115
|
-
...r.metadata,
|
|
116
|
-
},
|
|
117
|
-
})),
|
|
118
|
-
};
|
|
202
|
+
const { stdout } = await execFileAsync(cmd, args, {
|
|
203
|
+
maxBuffer: MAX_BUFFER,
|
|
204
|
+
timeout: timeoutMs ?? 30_000, // subprocess gets a longer default
|
|
205
|
+
});
|
|
206
|
+
return JSON.parse(stdout);
|
|
119
207
|
}
|
|
120
208
|
catch (error) {
|
|
121
|
-
throw new Error(`Failed to recall memories: ${error}
|
|
209
|
+
throw new Error(`Failed to recall memories: ${error}`, { cause: error });
|
|
122
210
|
}
|
|
123
211
|
}
|
|
124
212
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { MoltbotPluginAPI } from './types.js';
|
|
2
2
|
import { HindsightClient } from './client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Strip plugin-injected memory tags from content to prevent retain feedback loop.
|
|
5
|
+
* Removes <hindsight_memories> and <relevant_memories> blocks that were injected
|
|
6
|
+
* during before_agent_start so they don't get re-stored into the memory bank.
|
|
7
|
+
*/
|
|
8
|
+
export declare function stripMemoryTags(content: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Extract a recall query from a hook event's rawMessage or prompt.
|
|
11
|
+
*
|
|
12
|
+
* Prefers rawMessage (clean user text). Falls back to prompt, stripping
|
|
13
|
+
* envelope formatting (System: lines, [Channel ...] headers, [from: X] footers).
|
|
14
|
+
*
|
|
15
|
+
* Returns null when no usable query (< 5 chars) can be extracted.
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractRecallQuery(rawMessage: string | undefined, prompt: string | undefined): string | null;
|
|
3
18
|
export default function (api: MoltbotPluginAPI): void;
|
|
4
19
|
export declare function getClient(): HindsightClient | null;
|
package/dist/index.js
CHANGED
|
@@ -12,15 +12,83 @@ let usingExternalApi = false; // Track if using external API (skip daemon manage
|
|
|
12
12
|
let currentPluginConfig = null;
|
|
13
13
|
// Track which banks have had their mission set (to avoid re-setting on every request)
|
|
14
14
|
const banksWithMissionSet = new Set();
|
|
15
|
+
const inflightRecalls = new Map();
|
|
16
|
+
const RECALL_TIMEOUT_MS = 10_000;
|
|
17
|
+
// Cooldown + guard to prevent concurrent reinit attempts
|
|
18
|
+
let lastReinitAttempt = 0;
|
|
19
|
+
let isReinitInProgress = false;
|
|
20
|
+
const REINIT_COOLDOWN_MS = 30_000;
|
|
21
|
+
/**
|
|
22
|
+
* Lazy re-initialization after startup failure.
|
|
23
|
+
* Called by waitForReady when initPromise rejected but API may now be reachable.
|
|
24
|
+
* Throttled to one attempt per 30s to avoid hammering a down service.
|
|
25
|
+
*/
|
|
26
|
+
async function lazyReinit() {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
if (now - lastReinitAttempt < REINIT_COOLDOWN_MS || isReinitInProgress) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
isReinitInProgress = true;
|
|
32
|
+
lastReinitAttempt = now;
|
|
33
|
+
const config = currentPluginConfig;
|
|
34
|
+
if (!config) {
|
|
35
|
+
isReinitInProgress = false;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const externalApi = detectExternalApi(config);
|
|
39
|
+
if (!externalApi.apiUrl) {
|
|
40
|
+
isReinitInProgress = false;
|
|
41
|
+
return; // Only external API mode supports lazy reinit
|
|
42
|
+
}
|
|
43
|
+
console.log('[Hindsight] Attempting lazy re-initialization...');
|
|
44
|
+
try {
|
|
45
|
+
await checkExternalApiHealth(externalApi.apiUrl);
|
|
46
|
+
// Health check passed — set up env vars and create client
|
|
47
|
+
process.env.HINDSIGHT_EMBED_API_URL = externalApi.apiUrl;
|
|
48
|
+
if (externalApi.apiToken) {
|
|
49
|
+
process.env.HINDSIGHT_EMBED_API_TOKEN = externalApi.apiToken;
|
|
50
|
+
}
|
|
51
|
+
const llmConfig = detectLLMConfig(config);
|
|
52
|
+
client = new HindsightClient(buildClientOptions(llmConfig, config, externalApi));
|
|
53
|
+
const defaultBankId = deriveBankId(undefined, config);
|
|
54
|
+
client.setBankId(defaultBankId);
|
|
55
|
+
if (config.bankMission && !config.dynamicBankId) {
|
|
56
|
+
await client.setBankMission(config.bankMission);
|
|
57
|
+
}
|
|
58
|
+
usingExternalApi = true;
|
|
59
|
+
isInitialized = true;
|
|
60
|
+
// Replace the rejected initPromise with a resolved one
|
|
61
|
+
initPromise = Promise.resolve();
|
|
62
|
+
console.log('[Hindsight] ✓ Lazy re-initialization succeeded');
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.warn(`[Hindsight] Lazy re-initialization failed (will retry in ${REINIT_COOLDOWN_MS / 1000}s):`, error instanceof Error ? error.message : error);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
isReinitInProgress = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
15
71
|
// Global access for hooks (Moltbot loads hooks separately)
|
|
16
72
|
if (typeof global !== 'undefined') {
|
|
17
73
|
global.__hindsightClient = {
|
|
18
74
|
getClient: () => client,
|
|
19
75
|
waitForReady: async () => {
|
|
20
|
-
if (isInitialized)
|
|
76
|
+
if (isInitialized) {
|
|
21
77
|
return;
|
|
22
|
-
|
|
23
|
-
|
|
78
|
+
}
|
|
79
|
+
if (initPromise) {
|
|
80
|
+
try {
|
|
81
|
+
await initPromise;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Init failed (e.g., health check timeout at startup).
|
|
85
|
+
// Attempt lazy re-initialization so Hindsight recovers
|
|
86
|
+
// once the API becomes reachable again.
|
|
87
|
+
if (!isInitialized) {
|
|
88
|
+
await lazyReinit();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
24
92
|
},
|
|
25
93
|
/**
|
|
26
94
|
* Get a client configured for a specific agent context.
|
|
@@ -28,8 +96,9 @@ if (typeof global !== 'undefined') {
|
|
|
28
96
|
* Also ensures the bank mission is set on first use.
|
|
29
97
|
*/
|
|
30
98
|
getClientForContext: async (ctx) => {
|
|
31
|
-
if (!client)
|
|
99
|
+
if (!client) {
|
|
32
100
|
return null;
|
|
101
|
+
}
|
|
33
102
|
const config = currentPluginConfig || {};
|
|
34
103
|
const bankId = deriveBankId(ctx, config);
|
|
35
104
|
client.setBankId(bankId);
|
|
@@ -55,9 +124,54 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
55
124
|
const __dirname = dirname(__filename);
|
|
56
125
|
// Default bank name (fallback when channel context not available)
|
|
57
126
|
const DEFAULT_BANK_NAME = 'openclaw';
|
|
127
|
+
/**
|
|
128
|
+
* Strip plugin-injected memory tags from content to prevent retain feedback loop.
|
|
129
|
+
* Removes <hindsight_memories> and <relevant_memories> blocks that were injected
|
|
130
|
+
* during before_agent_start so they don't get re-stored into the memory bank.
|
|
131
|
+
*/
|
|
132
|
+
export function stripMemoryTags(content) {
|
|
133
|
+
content = content.replace(/<hindsight_memories>[\s\S]*?<\/hindsight_memories>/g, '');
|
|
134
|
+
content = content.replace(/<relevant_memories>[\s\S]*?<\/relevant_memories>/g, '');
|
|
135
|
+
return content;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Extract a recall query from a hook event's rawMessage or prompt.
|
|
139
|
+
*
|
|
140
|
+
* Prefers rawMessage (clean user text). Falls back to prompt, stripping
|
|
141
|
+
* envelope formatting (System: lines, [Channel ...] headers, [from: X] footers).
|
|
142
|
+
*
|
|
143
|
+
* Returns null when no usable query (< 5 chars) can be extracted.
|
|
144
|
+
*/
|
|
145
|
+
export function extractRecallQuery(rawMessage, prompt) {
|
|
146
|
+
let recallQuery = rawMessage;
|
|
147
|
+
if (!recallQuery || typeof recallQuery !== 'string' || recallQuery.trim().length < 5) {
|
|
148
|
+
recallQuery = prompt;
|
|
149
|
+
if (!recallQuery || typeof recallQuery !== 'string' || recallQuery.length < 5) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
// Strip envelope-formatted prompts from any channel
|
|
153
|
+
let cleaned = recallQuery;
|
|
154
|
+
// Remove leading "System: ..." lines (from prependSystemEvents)
|
|
155
|
+
cleaned = cleaned.replace(/^(?:System:.*\n)+\n?/, '');
|
|
156
|
+
// Remove session abort hint
|
|
157
|
+
cleaned = cleaned.replace(/^Note: The previous agent run was aborted[^\n]*\n\n/, '');
|
|
158
|
+
// Extract message after [ChannelName ...] envelope header
|
|
159
|
+
const envelopeMatch = cleaned.match(/\[[A-Z][A-Za-z]*(?:\s[^\]]+)?\]\s*([\s\S]+)$/);
|
|
160
|
+
if (envelopeMatch) {
|
|
161
|
+
cleaned = envelopeMatch[1];
|
|
162
|
+
}
|
|
163
|
+
// Remove trailing [from: SenderName] metadata (group chats)
|
|
164
|
+
cleaned = cleaned.replace(/\n\[from:[^\]]*\]\s*$/, '');
|
|
165
|
+
recallQuery = cleaned.trim() || recallQuery;
|
|
166
|
+
}
|
|
167
|
+
const trimmed = recallQuery.trim();
|
|
168
|
+
if (trimmed.length < 5)
|
|
169
|
+
return null;
|
|
170
|
+
return trimmed;
|
|
171
|
+
}
|
|
58
172
|
/**
|
|
59
173
|
* Derive a bank ID from the agent context.
|
|
60
|
-
* Creates
|
|
174
|
+
* Creates per-user banks: {messageProvider}-{senderId}
|
|
61
175
|
* Falls back to default bank when context is unavailable.
|
|
62
176
|
*/
|
|
63
177
|
function deriveBankId(ctx, pluginConfig) {
|
|
@@ -68,9 +182,9 @@ function deriveBankId(ctx, pluginConfig) {
|
|
|
68
182
|
: DEFAULT_BANK_NAME;
|
|
69
183
|
}
|
|
70
184
|
const channelType = ctx?.messageProvider || 'unknown';
|
|
71
|
-
const
|
|
72
|
-
// Build bank ID: {prefix?}-{channelType}-{
|
|
73
|
-
const baseBankId = `${channelType}-${
|
|
185
|
+
const userId = ctx?.senderId || 'default';
|
|
186
|
+
// Build bank ID: {prefix?}-{channelType}-{senderId}
|
|
187
|
+
const baseBankId = `${channelType}-${userId}`;
|
|
74
188
|
return pluginConfig.bankIdPrefix
|
|
75
189
|
? `${pluginConfig.bankIdPrefix}-${baseBankId}`
|
|
76
190
|
: baseBankId;
|
|
@@ -181,22 +295,48 @@ function detectExternalApi(pluginConfig) {
|
|
|
181
295
|
const apiToken = process.env.HINDSIGHT_EMBED_API_TOKEN || pluginConfig?.hindsightApiToken || null;
|
|
182
296
|
return { apiUrl, apiToken };
|
|
183
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Build HindsightClientOptions from LLM config, plugin config, and external API settings.
|
|
300
|
+
*/
|
|
301
|
+
function buildClientOptions(llmConfig, pluginCfg, externalApi) {
|
|
302
|
+
return {
|
|
303
|
+
llmProvider: llmConfig.provider,
|
|
304
|
+
llmApiKey: llmConfig.apiKey,
|
|
305
|
+
llmModel: llmConfig.model,
|
|
306
|
+
embedVersion: pluginCfg.embedVersion,
|
|
307
|
+
embedPackagePath: pluginCfg.embedPackagePath,
|
|
308
|
+
apiUrl: externalApi.apiUrl ?? undefined,
|
|
309
|
+
apiToken: externalApi.apiToken ?? undefined,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
184
312
|
/**
|
|
185
313
|
* Health check for external Hindsight API.
|
|
314
|
+
* Retries up to 3 times with 2s delay — container DNS may not be ready on first boot.
|
|
186
315
|
*/
|
|
187
316
|
async function checkExternalApiHealth(apiUrl) {
|
|
188
317
|
const healthUrl = `${apiUrl.replace(/\/$/, '')}/health`;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
318
|
+
const maxRetries = 3;
|
|
319
|
+
const retryDelay = 2000;
|
|
320
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
321
|
+
try {
|
|
322
|
+
console.log(`[Hindsight] Checking external API health at ${healthUrl}... (attempt ${attempt}/${maxRetries})`);
|
|
323
|
+
const response = await fetch(healthUrl, { signal: AbortSignal.timeout(10000) });
|
|
324
|
+
if (!response.ok) {
|
|
325
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
326
|
+
}
|
|
327
|
+
const data = await response.json();
|
|
328
|
+
console.log(`[Hindsight] External API health: ${JSON.stringify(data)}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
if (attempt < maxRetries) {
|
|
333
|
+
console.log(`[Hindsight] Health check attempt ${attempt} failed, retrying in ${retryDelay}ms...`);
|
|
334
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
throw new Error(`Cannot connect to external Hindsight API at ${apiUrl}: ${error}`, { cause: error });
|
|
338
|
+
}
|
|
194
339
|
}
|
|
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
340
|
}
|
|
201
341
|
}
|
|
202
342
|
function getPluginConfig(api) {
|
|
@@ -277,9 +417,9 @@ export default function (api) {
|
|
|
277
417
|
// External API mode - check health, skip daemon startup
|
|
278
418
|
console.log('[Hindsight] External API mode - skipping local daemon...');
|
|
279
419
|
await checkExternalApiHealth(externalApi.apiUrl);
|
|
280
|
-
// Initialize client
|
|
281
|
-
console.log('[Hindsight] Creating HindsightClient...');
|
|
282
|
-
client = new HindsightClient(llmConfig
|
|
420
|
+
// Initialize client with direct HTTP mode
|
|
421
|
+
console.log('[Hindsight] Creating HindsightClient (HTTP mode)...');
|
|
422
|
+
client = new HindsightClient(buildClientOptions(llmConfig, pluginConfig, externalApi));
|
|
283
423
|
// Set default bank (will be overridden per-request when dynamic bank IDs are enabled)
|
|
284
424
|
const defaultBankId = deriveBankId(undefined, pluginConfig);
|
|
285
425
|
console.log(`[Hindsight] Default bank: ${defaultBankId}`);
|
|
@@ -300,9 +440,9 @@ export default function (api) {
|
|
|
300
440
|
// Start the embedded server
|
|
301
441
|
console.log('[Hindsight] Starting embedded server...');
|
|
302
442
|
await embedManager.start();
|
|
303
|
-
// Initialize client
|
|
304
|
-
console.log('[Hindsight] Creating HindsightClient...');
|
|
305
|
-
client = new HindsightClient(llmConfig
|
|
443
|
+
// Initialize client (local daemon mode — no apiUrl)
|
|
444
|
+
console.log('[Hindsight] Creating HindsightClient (subprocess mode)...');
|
|
445
|
+
client = new HindsightClient(buildClientOptions(llmConfig, pluginConfig, { apiUrl: null, apiToken: null }));
|
|
306
446
|
// Set default bank (will be overridden per-request when dynamic bank IDs are enabled)
|
|
307
447
|
const defaultBankId = deriveBankId(undefined, pluginConfig);
|
|
308
448
|
console.log(`[Hindsight] Default bank: ${defaultBankId}`);
|
|
@@ -322,7 +462,8 @@ export default function (api) {
|
|
|
322
462
|
throw error;
|
|
323
463
|
}
|
|
324
464
|
})();
|
|
325
|
-
//
|
|
465
|
+
// Suppress unhandled rejection — service.start() will await and handle errors
|
|
466
|
+
initPromise.catch(() => { });
|
|
326
467
|
// Register background service for cleanup
|
|
327
468
|
console.log('[Hindsight] Registering service...');
|
|
328
469
|
api.registerService({
|
|
@@ -387,7 +528,7 @@ export default function (api) {
|
|
|
387
528
|
process.env.HINDSIGHT_EMBED_API_TOKEN = externalApi.apiToken;
|
|
388
529
|
}
|
|
389
530
|
await checkExternalApiHealth(externalApi.apiUrl);
|
|
390
|
-
client = new HindsightClient(llmConfig
|
|
531
|
+
client = new HindsightClient(buildClientOptions(llmConfig, reinitPluginConfig, externalApi));
|
|
391
532
|
const defaultBankId = deriveBankId(undefined, reinitPluginConfig);
|
|
392
533
|
client.setBankId(defaultBankId);
|
|
393
534
|
if (reinitPluginConfig.bankMission && !reinitPluginConfig.dynamicBankId) {
|
|
@@ -400,7 +541,7 @@ export default function (api) {
|
|
|
400
541
|
// Local daemon mode
|
|
401
542
|
embedManager = new HindsightEmbedManager(apiPort, llmConfig.provider, llmConfig.apiKey, llmConfig.model, llmConfig.baseUrl, reinitPluginConfig.daemonIdleTimeout, reinitPluginConfig.embedVersion, reinitPluginConfig.embedPackagePath);
|
|
402
543
|
await embedManager.start();
|
|
403
|
-
client = new HindsightClient(llmConfig
|
|
544
|
+
client = new HindsightClient(buildClientOptions(llmConfig, reinitPluginConfig, { apiUrl: null, apiToken: null }));
|
|
404
545
|
const defaultBankId = deriveBankId(undefined, reinitPluginConfig);
|
|
405
546
|
client.setBankId(defaultBankId);
|
|
406
547
|
if (reinitPluginConfig.bankMission && !reinitPluginConfig.dynamicBankId) {
|
|
@@ -452,31 +593,17 @@ export default function (api) {
|
|
|
452
593
|
// Derive bank ID from context
|
|
453
594
|
const bankId = deriveBankId(ctx, pluginConfig);
|
|
454
595
|
console.log(`[Hindsight] before_agent_start - bank: ${bankId}, channel: ${ctx?.messageProvider}/${ctx?.channelId}`);
|
|
455
|
-
// Get the user's latest message for recall
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
if (!
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
// Strip envelope-formatted prompts from any channel
|
|
462
|
-
// The prompt may contain: System: lines, abort hints, [Channel ...] header, [from: ...] suffix
|
|
463
|
-
let cleaned = prompt;
|
|
464
|
-
// Remove leading "System: ..." lines (from prependSystemEvents)
|
|
465
|
-
cleaned = cleaned.replace(/^(?:System:.*\n)+\n?/, '');
|
|
466
|
-
// Remove session abort hint
|
|
467
|
-
cleaned = cleaned.replace(/^Note: The previous agent run was aborted[^\n]*\n\n/, '');
|
|
468
|
-
// Extract message after [ChannelName ...] envelope header
|
|
469
|
-
// Handles any channel: Telegram, Slack, Discord, WhatsApp, Signal, etc.
|
|
470
|
-
// Uses [\s\S]+ instead of .+ to support multiline messages
|
|
471
|
-
const envelopeMatch = cleaned.match(/\[[A-Z][A-Za-z]*(?:\s[^\]]+)?\]\s*([\s\S]+)$/);
|
|
472
|
-
if (envelopeMatch) {
|
|
473
|
-
cleaned = envelopeMatch[1];
|
|
596
|
+
// Get the user's latest message for recall — only the raw user text, not the full prompt
|
|
597
|
+
// rawMessage is clean user text; prompt includes envelope, system events, media notes, etc.
|
|
598
|
+
const extracted = extractRecallQuery(event.rawMessage, event.prompt);
|
|
599
|
+
if (!extracted) {
|
|
600
|
+
return;
|
|
474
601
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (prompt.length
|
|
479
|
-
|
|
602
|
+
let prompt = extracted;
|
|
603
|
+
// Truncate — Hindsight API recall has a 500 token limit; 800 chars stays safely under even with non-ASCII
|
|
604
|
+
const MAX_RECALL_QUERY_CHARS = 800;
|
|
605
|
+
if (prompt.length > MAX_RECALL_QUERY_CHARS) {
|
|
606
|
+
prompt = prompt.substring(0, MAX_RECALL_QUERY_CHARS);
|
|
480
607
|
}
|
|
481
608
|
// Wait for client to be ready
|
|
482
609
|
const clientGlobal = global.__hindsightClient;
|
|
@@ -492,11 +619,20 @@ export default function (api) {
|
|
|
492
619
|
return;
|
|
493
620
|
}
|
|
494
621
|
console.log(`[Hindsight] Auto-recall for bank ${bankId}, prompt: ${prompt.substring(0, 50)}`);
|
|
495
|
-
// Recall
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
622
|
+
// Recall with deduplication: reuse in-flight request for same bank
|
|
623
|
+
const recallKey = bankId;
|
|
624
|
+
const existing = inflightRecalls.get(recallKey);
|
|
625
|
+
let recallPromise;
|
|
626
|
+
if (existing) {
|
|
627
|
+
console.log(`[Hindsight] Reusing in-flight recall for bank ${bankId}`);
|
|
628
|
+
recallPromise = existing;
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
recallPromise = client.recall({ query: prompt, max_tokens: 2048 }, RECALL_TIMEOUT_MS);
|
|
632
|
+
inflightRecalls.set(recallKey, recallPromise);
|
|
633
|
+
void recallPromise.catch(() => { }).finally(() => inflightRecalls.delete(recallKey));
|
|
634
|
+
}
|
|
635
|
+
const response = await recallPromise;
|
|
500
636
|
if (!response.results || response.results.length === 0) {
|
|
501
637
|
console.log('[Hindsight] No memories found for auto-recall');
|
|
502
638
|
return;
|
|
@@ -504,7 +640,7 @@ export default function (api) {
|
|
|
504
640
|
// Format memories as JSON with all fields from recall
|
|
505
641
|
const memoriesJson = JSON.stringify(response.results, null, 2);
|
|
506
642
|
const contextMessage = `<hindsight_memories>
|
|
507
|
-
Relevant memories from past conversations (
|
|
643
|
+
Relevant memories from past conversations (prioritize recent when conflicting):
|
|
508
644
|
${memoriesJson}
|
|
509
645
|
|
|
510
646
|
User message: ${prompt}
|
|
@@ -514,7 +650,15 @@ User message: ${prompt}
|
|
|
514
650
|
return { prependContext: contextMessage };
|
|
515
651
|
}
|
|
516
652
|
catch (error) {
|
|
517
|
-
|
|
653
|
+
if (error instanceof DOMException && error.name === 'TimeoutError') {
|
|
654
|
+
console.warn(`[Hindsight] Auto-recall timed out after ${RECALL_TIMEOUT_MS}ms, skipping memory injection`);
|
|
655
|
+
}
|
|
656
|
+
else if (error instanceof Error && error.name === 'AbortError') {
|
|
657
|
+
console.warn(`[Hindsight] Auto-recall aborted after ${RECALL_TIMEOUT_MS}ms, skipping memory injection`);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
console.error('[Hindsight] Auto-recall error:', error);
|
|
661
|
+
}
|
|
518
662
|
return;
|
|
519
663
|
}
|
|
520
664
|
});
|
|
@@ -565,10 +709,7 @@ User message: ${prompt}
|
|
|
565
709
|
.join('\n');
|
|
566
710
|
}
|
|
567
711
|
// Strip plugin-injected memory tags to prevent feedback loop
|
|
568
|
-
|
|
569
|
-
content = content.replace(/<hindsight_memories>[\s\S]*?<\/hindsight_memories>/g, '');
|
|
570
|
-
// Remove any <relevant_memories> blocks (legacy/alternative format)
|
|
571
|
-
content = content.replace(/<relevant_memories>[\s\S]*?<\/relevant_memories>/g, '');
|
|
712
|
+
content = stripMemoryTags(content);
|
|
572
713
|
return `[role: ${role}]\n${content}\n[${role}:end]`;
|
|
573
714
|
})
|
|
574
715
|
.join('\n\n');
|
|
@@ -585,7 +726,7 @@ User message: ${prompt}
|
|
|
585
726
|
document_id: documentId,
|
|
586
727
|
metadata: {
|
|
587
728
|
retained_at: new Date().toISOString(),
|
|
588
|
-
message_count: event.messages.length,
|
|
729
|
+
message_count: String(event.messages.length),
|
|
589
730
|
channel_type: effectiveCtx?.messageProvider,
|
|
590
731
|
channel_id: effectiveCtx?.channelId,
|
|
591
732
|
sender_id: effectiveCtx?.senderId,
|
package/dist/types.d.ts
CHANGED
|
@@ -61,15 +61,23 @@ export interface RecallRequest {
|
|
|
61
61
|
}
|
|
62
62
|
export interface RecallResponse {
|
|
63
63
|
results: MemoryResult[];
|
|
64
|
+
entities: Record<string, unknown> | null;
|
|
65
|
+
trace: unknown | null;
|
|
66
|
+
chunks: unknown | null;
|
|
64
67
|
}
|
|
65
68
|
export interface MemoryResult {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
id: string;
|
|
70
|
+
text: string;
|
|
71
|
+
type: string;
|
|
72
|
+
entities: string[];
|
|
73
|
+
context: string;
|
|
74
|
+
occurred_start: string | null;
|
|
75
|
+
occurred_end: string | null;
|
|
76
|
+
mentioned_at: string | null;
|
|
77
|
+
document_id: string | null;
|
|
78
|
+
metadata: Record<string, unknown> | null;
|
|
79
|
+
chunk_id: string | null;
|
|
80
|
+
tags: string[];
|
|
73
81
|
}
|
|
74
82
|
export interface CreateBankRequest {
|
|
75
83
|
name: string;
|
package/openclaw.plugin.json
CHANGED
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dynamicBankId": {
|
|
59
59
|
"type": "boolean",
|
|
60
|
-
"description": "Enable per-
|
|
60
|
+
"description": "Enable per-user memory banks. When true, memories are isolated by user per channel (e.g., slack-U123, telegram-456789). When false, all users share a single 'openclaw' bank.",
|
|
61
61
|
"default": true
|
|
62
62
|
},
|
|
63
63
|
"bankIdPrefix": {
|
|
64
64
|
"type": "string",
|
|
65
|
-
"description": "Optional prefix for bank IDs (e.g., 'prod' results in 'prod-slack-
|
|
65
|
+
"description": "Optional prefix for bank IDs (e.g., 'prod' results in 'prod-slack-U123'). Useful for separating environments."
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
"additionalProperties": false
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vectorize-io/hindsight-openclaw",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"description": "Hindsight memory plugin for OpenClaw - biomimetic long-term memory with fact extraction",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,8 +34,9 @@
|
|
|
34
34
|
"build": "tsc",
|
|
35
35
|
"dev": "tsc --watch",
|
|
36
36
|
"clean": "rm -rf dist",
|
|
37
|
-
"test": "vitest run",
|
|
38
|
-
"test:watch": "vitest",
|
|
37
|
+
"test": "vitest run src",
|
|
38
|
+
"test:watch": "vitest src",
|
|
39
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
39
40
|
"prepublishOnly": "npm run clean && npm run build"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|