@clawling/clawchat-plugin-openclaw 2026.5.12-28
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/INSTALL.md +64 -0
- package/README.md +227 -0
- package/dist/index.js +20 -0
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +263 -0
- package/dist/src/api-types.js +17 -0
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/buffered-stream.js +177 -0
- package/dist/src/channel.js +66 -0
- package/dist/src/channel.setup.js +119 -0
- package/dist/src/clawchat-memory.js +403 -0
- package/dist/src/clawchat-metadata.js +310 -0
- package/dist/src/client.js +35 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +274 -0
- package/dist/src/group-message-coalescer.js +119 -0
- package/dist/src/inbound.js +170 -0
- package/dist/src/llm-context-debug.js +86 -0
- package/dist/src/login.runtime.js +204 -0
- package/dist/src/media-runtime.js +85 -0
- package/dist/src/message-mapper.js +146 -0
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +628 -0
- package/dist/src/plugin-prompts.js +89 -0
- package/dist/src/profile-prompt.js +269 -0
- package/dist/src/profile-sync.js +110 -0
- package/dist/src/prompt-injection.js +25 -0
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +33 -0
- package/dist/src/reply-dispatcher.js +422 -0
- package/dist/src/runtime.js +1254 -0
- package/dist/src/storage.js +525 -0
- package/dist/src/streaming.js +65 -0
- package/dist/src/terminal-send.js +36 -0
- package/dist/src/tools-schema.js +208 -0
- package/dist/src/tools.js +920 -0
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +24 -0
- package/openclaw.plugin.json +169 -0
- package/package.json +80 -0
- package/prompts/default-group-bio.md +19 -0
- package/prompts/default-owner-behavior.md +27 -0
- package/prompts/platform.md +13 -0
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +91 -0
- package/src/api-client.test.ts +827 -0
- package/src/api-client.ts +414 -0
- package/src/api-types.ts +146 -0
- package/src/channel.outbound.test.ts +433 -0
- package/src/channel.setup.ts +145 -0
- package/src/channel.test.ts +262 -0
- package/src/channel.ts +81 -0
- package/src/clawchat-memory.test.ts +480 -0
- package/src/clawchat-memory.ts +533 -0
- package/src/clawchat-metadata.test.ts +477 -0
- package/src/clawchat-metadata.ts +429 -0
- package/src/client.test.ts +169 -0
- package/src/client.ts +56 -0
- package/src/commands.test.ts +39 -0
- package/src/commands.ts +41 -0
- package/src/config.test.ts +344 -0
- package/src/config.ts +404 -0
- package/src/group-message-coalescer.test.ts +237 -0
- package/src/group-message-coalescer.ts +171 -0
- package/src/inbound.test.ts +508 -0
- package/src/inbound.ts +278 -0
- package/src/llm-context-debug.test.ts +55 -0
- package/src/llm-context-debug.ts +139 -0
- package/src/login.runtime.test.ts +737 -0
- package/src/login.runtime.ts +277 -0
- package/src/manifest.test.ts +352 -0
- package/src/media-runtime.test.ts +207 -0
- package/src/media-runtime.ts +152 -0
- package/src/message-mapper.test.ts +201 -0
- package/src/message-mapper.ts +174 -0
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +1269 -0
- package/src/outbound.ts +803 -0
- package/src/plugin-entry.test.ts +38 -0
- package/src/plugin-prompts.test.ts +94 -0
- package/src/plugin-prompts.ts +107 -0
- package/src/profile-prompt.test.ts +274 -0
- package/src/profile-prompt.ts +351 -0
- package/src/profile-sync.test.ts +539 -0
- package/src/profile-sync.ts +191 -0
- package/src/prompt-injection.test.ts +39 -0
- package/src/prompt-injection.ts +45 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.test.ts +39 -0
- package/src/protocol.ts +42 -0
- package/src/reply-dispatcher.test.ts +1324 -0
- package/src/reply-dispatcher.ts +555 -0
- package/src/runtime.test.ts +4719 -0
- package/src/runtime.ts +1493 -0
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +560 -0
- package/src/storage.ts +807 -0
- package/src/terminal-send.test.ts +81 -0
- package/src/terminal-send.ts +56 -0
- package/src/tools-schema.ts +337 -0
- package/src/tools.test.ts +933 -0
- package/src/tools.ts +1185 -0
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1217 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { getDefaultGroupBioPrompt, getDefaultOwnerBehaviorPrompt } from "./plugin-prompts.js";
|
|
5
|
+
const metadataStart = "<!-- clawchat:metadata:start -->";
|
|
6
|
+
const metadataEnd = "<!-- clawchat:metadata:end -->";
|
|
7
|
+
function assertValidTargetId(target) {
|
|
8
|
+
if (target.targetType !== "owner" && target.targetType !== "user" && target.targetType !== "group") {
|
|
9
|
+
throw new Error("Invalid clawchat memory targetType");
|
|
10
|
+
}
|
|
11
|
+
if (typeof target.targetId !== "string") {
|
|
12
|
+
throw new Error("Invalid clawchat memory targetId: targetId must be a string");
|
|
13
|
+
}
|
|
14
|
+
if (target.targetId.length === 0) {
|
|
15
|
+
throw new Error("Invalid clawchat memory targetId: targetId is required");
|
|
16
|
+
}
|
|
17
|
+
if (target.targetId === "." || target.targetId === ".." || target.targetId.includes("..")) {
|
|
18
|
+
throw new Error("Invalid clawchat memory targetId");
|
|
19
|
+
}
|
|
20
|
+
if (/[\/\\\p{Cc}]/u.test(target.targetId)) {
|
|
21
|
+
throw new Error("Invalid clawchat memory targetId");
|
|
22
|
+
}
|
|
23
|
+
if (target.targetType === "owner" && target.targetId !== "owner") {
|
|
24
|
+
throw new Error("Invalid clawchat memory owner targetId: owner targetId must be owner");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function resolveClawChatMemoryPath(root, target) {
|
|
28
|
+
assertValidTargetId(target);
|
|
29
|
+
if (target.targetType === "owner") {
|
|
30
|
+
return path.resolve(root, "owner.md");
|
|
31
|
+
}
|
|
32
|
+
if (target.targetType === "user") {
|
|
33
|
+
return path.resolve(root, "users", `${target.targetId}.md`);
|
|
34
|
+
}
|
|
35
|
+
return path.resolve(root, "groups", `${target.targetId}.md`);
|
|
36
|
+
}
|
|
37
|
+
async function pathExists(candidate) {
|
|
38
|
+
try {
|
|
39
|
+
await fs.lstat(candidate);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.code === "ENOENT") {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function assertInsideRoot(root, candidate) {
|
|
50
|
+
const relative = path.relative(root, candidate);
|
|
51
|
+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Resolved clawchat memory path is outside root: ${candidate}`);
|
|
55
|
+
}
|
|
56
|
+
async function assertExistingDirectorySafe(rootRealPath, dirPath) {
|
|
57
|
+
let stat;
|
|
58
|
+
try {
|
|
59
|
+
stat = await fs.lstat(dirPath);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error.code === "ENOENT") {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
if (stat.isSymbolicLink()) {
|
|
68
|
+
throw new Error(`Unsafe clawchat memory directory symlink: ${dirPath}`);
|
|
69
|
+
}
|
|
70
|
+
if (!stat.isDirectory()) {
|
|
71
|
+
throw new Error(`Unsafe clawchat memory parent is not a directory: ${dirPath}`);
|
|
72
|
+
}
|
|
73
|
+
assertInsideRoot(rootRealPath, await fs.realpath(dirPath));
|
|
74
|
+
}
|
|
75
|
+
async function listSafeMemoryFiles(root, rootRealPath, targetType) {
|
|
76
|
+
if (targetType === "owner") {
|
|
77
|
+
return [{ targetType: "owner", targetId: "owner" }];
|
|
78
|
+
}
|
|
79
|
+
const dirPath = path.resolve(root, targetType === "user" ? "users" : "groups");
|
|
80
|
+
await assertExistingDirectorySafe(rootRealPath, dirPath);
|
|
81
|
+
if (!(await pathExists(dirPath))) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
85
|
+
const targets = [];
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (!entry.name.endsWith(".md")) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const targetId = entry.name.slice(0, -3);
|
|
91
|
+
assertValidTargetId({ targetType, targetId });
|
|
92
|
+
targets.push({ targetType, targetId });
|
|
93
|
+
}
|
|
94
|
+
targets.sort((a, b) => a.targetId.localeCompare(b.targetId));
|
|
95
|
+
return targets;
|
|
96
|
+
}
|
|
97
|
+
export async function ensureClawChatMemoryTargetSafe(root, target) {
|
|
98
|
+
const targetPath = resolveClawChatMemoryPath(root, target);
|
|
99
|
+
const rootPath = path.resolve(root);
|
|
100
|
+
const rootRealPath = (await pathExists(rootPath)) ? await fs.realpath(rootPath) : rootPath;
|
|
101
|
+
assertInsideRoot(rootPath, targetPath);
|
|
102
|
+
if (target.targetType !== "owner") {
|
|
103
|
+
await assertExistingDirectorySafe(rootRealPath, path.dirname(targetPath));
|
|
104
|
+
}
|
|
105
|
+
let stat;
|
|
106
|
+
try {
|
|
107
|
+
stat = await fs.lstat(targetPath);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error.code === "ENOENT") {
|
|
111
|
+
return targetPath;
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
if (stat.isSymbolicLink()) {
|
|
116
|
+
throw new Error(`Unsafe clawchat memory target symlink: ${targetPath}`);
|
|
117
|
+
}
|
|
118
|
+
if (!stat.isFile()) {
|
|
119
|
+
throw new Error(`Unsafe clawchat memory target is not a regular file: ${targetPath}`);
|
|
120
|
+
}
|
|
121
|
+
assertInsideRoot(rootRealPath, await fs.realpath(targetPath));
|
|
122
|
+
return targetPath;
|
|
123
|
+
}
|
|
124
|
+
function normalizeLineEndings(value) {
|
|
125
|
+
return value.replace(/\r\n?/g, "\n");
|
|
126
|
+
}
|
|
127
|
+
function normalizeMetadataValue(value) {
|
|
128
|
+
return value.replace(/[\r\n]+/g, " ");
|
|
129
|
+
}
|
|
130
|
+
function consumeLineEnding(value, index) {
|
|
131
|
+
if (value.startsWith("\r\n", index)) {
|
|
132
|
+
return 2;
|
|
133
|
+
}
|
|
134
|
+
if (value[index] === "\n" || value[index] === "\r") {
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
function splitRawBodySuffix(rawBodySuffix) {
|
|
140
|
+
let offset = consumeLineEnding(rawBodySuffix, 0);
|
|
141
|
+
if (offset === 0) {
|
|
142
|
+
return { prefix: "", rawBody: rawBodySuffix };
|
|
143
|
+
}
|
|
144
|
+
offset += consumeLineEnding(rawBodySuffix, offset);
|
|
145
|
+
return { prefix: rawBodySuffix.slice(0, offset), rawBody: rawBodySuffix.slice(offset) };
|
|
146
|
+
}
|
|
147
|
+
function parseMetadataBlock(rawMetadataBlock) {
|
|
148
|
+
const metadata = {};
|
|
149
|
+
const lines = normalizeLineEndings(rawMetadataBlock).split("\n");
|
|
150
|
+
for (const line of lines.slice(1, -1)) {
|
|
151
|
+
const separatorIndex = line.indexOf(":");
|
|
152
|
+
if (separatorIndex <= 0) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
156
|
+
if (key.length === 0) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
metadata[key] = line.slice(separatorIndex + 1).trimStart();
|
|
160
|
+
}
|
|
161
|
+
return metadata;
|
|
162
|
+
}
|
|
163
|
+
function parseClawChatMemoryRaw(raw) {
|
|
164
|
+
const firstLineEnd = raw.indexOf("\n");
|
|
165
|
+
const firstLineRawEnd = firstLineEnd === -1 ? raw.length : firstLineEnd;
|
|
166
|
+
const firstLineContentEnd = raw[firstLineRawEnd - 1] === "\r" ? firstLineRawEnd - 1 : firstLineRawEnd;
|
|
167
|
+
if (raw.slice(0, firstLineContentEnd) !== metadataStart) {
|
|
168
|
+
return { metadata: {}, body: normalizeLineEndings(raw), rawMetadataBlock: null, rawBodyPrefix: "", rawBody: raw };
|
|
169
|
+
}
|
|
170
|
+
let lineStart = firstLineEnd === -1 ? raw.length : firstLineEnd + 1;
|
|
171
|
+
while (lineStart < raw.length) {
|
|
172
|
+
const lineEnd = raw.indexOf("\n", lineStart);
|
|
173
|
+
const lineRawEnd = lineEnd === -1 ? raw.length : lineEnd;
|
|
174
|
+
const lineContentEnd = raw[lineRawEnd - 1] === "\r" ? lineRawEnd - 1 : lineRawEnd;
|
|
175
|
+
if (raw.slice(lineStart, lineContentEnd) === metadataEnd) {
|
|
176
|
+
const rawMetadataBlock = raw.slice(0, lineContentEnd);
|
|
177
|
+
const { prefix, rawBody } = splitRawBodySuffix(raw.slice(lineContentEnd));
|
|
178
|
+
return {
|
|
179
|
+
metadata: parseMetadataBlock(rawMetadataBlock),
|
|
180
|
+
body: normalizeLineEndings(rawBody),
|
|
181
|
+
rawMetadataBlock,
|
|
182
|
+
rawBodyPrefix: prefix,
|
|
183
|
+
rawBody,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (lineEnd === -1) {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
lineStart = lineEnd + 1;
|
|
190
|
+
}
|
|
191
|
+
return { metadata: {}, body: normalizeLineEndings(raw), rawMetadataBlock: null, rawBodyPrefix: "", rawBody: raw };
|
|
192
|
+
}
|
|
193
|
+
function formatMetadataBlock(metadata) {
|
|
194
|
+
const lines = [metadataStart];
|
|
195
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
196
|
+
lines.push(`${key}: ${normalizeMetadataValue(value)}`);
|
|
197
|
+
}
|
|
198
|
+
lines.push(metadataEnd);
|
|
199
|
+
return lines.join("\n");
|
|
200
|
+
}
|
|
201
|
+
function formatRawMemoryFile(parsed, rawBody) {
|
|
202
|
+
if (parsed.rawMetadataBlock === null) {
|
|
203
|
+
return rawBody;
|
|
204
|
+
}
|
|
205
|
+
if (rawBody.length === 0) {
|
|
206
|
+
return parsed.rawMetadataBlock;
|
|
207
|
+
}
|
|
208
|
+
return `${parsed.rawMetadataBlock}${parsed.rawBodyPrefix || "\n\n"}${rawBody}`;
|
|
209
|
+
}
|
|
210
|
+
function rawBodySuffixForMetadataReplacement(parsed) {
|
|
211
|
+
if (parsed.rawMetadataBlock === null) {
|
|
212
|
+
return parsed.rawBody.length === 0 ? "" : `\n\n${parsed.rawBody}`;
|
|
213
|
+
}
|
|
214
|
+
return `${parsed.rawBodyPrefix}${parsed.rawBody}`;
|
|
215
|
+
}
|
|
216
|
+
function metadataWithInitialDefaults(target, exists, metadata) {
|
|
217
|
+
if (exists) {
|
|
218
|
+
return metadata;
|
|
219
|
+
}
|
|
220
|
+
if (target.targetType === "owner" &&
|
|
221
|
+
Object.prototype.hasOwnProperty.call(metadata, "agent_behavior") &&
|
|
222
|
+
metadata.agent_behavior.trim().length === 0) {
|
|
223
|
+
return { ...metadata, agent_behavior: getDefaultOwnerBehaviorPrompt() };
|
|
224
|
+
}
|
|
225
|
+
if (target.targetType === "group" &&
|
|
226
|
+
Object.prototype.hasOwnProperty.call(metadata, "group_description") &&
|
|
227
|
+
metadata.group_description.trim().length === 0) {
|
|
228
|
+
return { ...metadata, group_description: getDefaultGroupBioPrompt() };
|
|
229
|
+
}
|
|
230
|
+
return metadata;
|
|
231
|
+
}
|
|
232
|
+
function normalizeWithRawBoundaryMap(raw) {
|
|
233
|
+
let normalized = "";
|
|
234
|
+
const rawBoundaries = [0];
|
|
235
|
+
let rawIndex = 0;
|
|
236
|
+
while (rawIndex < raw.length) {
|
|
237
|
+
if (raw[rawIndex] === "\r") {
|
|
238
|
+
normalized += "\n";
|
|
239
|
+
rawIndex += raw[rawIndex + 1] === "\n" ? 2 : 1;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
normalized += raw[rawIndex];
|
|
243
|
+
rawIndex += 1;
|
|
244
|
+
}
|
|
245
|
+
rawBoundaries.push(rawIndex);
|
|
246
|
+
}
|
|
247
|
+
return { normalized, rawBoundaries };
|
|
248
|
+
}
|
|
249
|
+
async function readExistingClawChatMemoryFile(root, target) {
|
|
250
|
+
const targetPath = await ensureClawChatMemoryTargetSafe(root, target);
|
|
251
|
+
let raw;
|
|
252
|
+
try {
|
|
253
|
+
raw = await fs.readFile(targetPath, "utf8");
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (error.code === "ENOENT") {
|
|
257
|
+
return {
|
|
258
|
+
targetPath,
|
|
259
|
+
exists: false,
|
|
260
|
+
parsed: { metadata: {}, body: "", rawMetadataBlock: null, rawBodyPrefix: "", rawBody: "" },
|
|
261
|
+
raw: "",
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
return { targetPath, exists: true, parsed: parseClawChatMemoryRaw(raw), raw };
|
|
267
|
+
}
|
|
268
|
+
async function writeFileAtomically(targetPath, content) {
|
|
269
|
+
const dir = path.dirname(targetPath);
|
|
270
|
+
await fs.mkdir(dir, { recursive: true });
|
|
271
|
+
const tempPath = path.join(dir, `.${path.basename(targetPath)}.${randomUUID()}.tmp`);
|
|
272
|
+
try {
|
|
273
|
+
await fs.writeFile(tempPath, content, "utf8");
|
|
274
|
+
await fs.rename(tempPath, targetPath);
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export async function readClawChatMemoryFile(root, target) {
|
|
282
|
+
const file = await readExistingClawChatMemoryFile(root, target);
|
|
283
|
+
return {
|
|
284
|
+
exists: file.exists,
|
|
285
|
+
metadata: file.parsed.metadata,
|
|
286
|
+
body: file.parsed.body,
|
|
287
|
+
raw: file.raw,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function normalizeSearchParams(params) {
|
|
291
|
+
const query = params.query.trim();
|
|
292
|
+
if (query.length === 0) {
|
|
293
|
+
throw new Error("clawchat memory search query is required");
|
|
294
|
+
}
|
|
295
|
+
const targetTypes = params.targetTypes ?? ["owner", "user", "group"];
|
|
296
|
+
if (targetTypes.length === 0 ||
|
|
297
|
+
targetTypes.some((targetType) => targetType !== "owner" && targetType !== "user" && targetType !== "group")) {
|
|
298
|
+
throw new Error("Invalid clawchat memory search targetTypes");
|
|
299
|
+
}
|
|
300
|
+
const maxResults = params.maxResults ?? 10;
|
|
301
|
+
if (!Number.isInteger(maxResults) || maxResults < 1 || maxResults > 50) {
|
|
302
|
+
throw new Error("clawchat memory search maxResults must be between 1 and 50");
|
|
303
|
+
}
|
|
304
|
+
return { query, queryLower: query.toLocaleLowerCase(), targetTypes, maxResults };
|
|
305
|
+
}
|
|
306
|
+
function firstMatchingLine(value, queryLower) {
|
|
307
|
+
for (const line of normalizeLineEndings(value).split("\n")) {
|
|
308
|
+
if (line.toLocaleLowerCase().includes(queryLower)) {
|
|
309
|
+
return line.length <= 300 ? line : `${line.slice(0, 297)}...`;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
function buildSearchMatch(target, file, queryLower) {
|
|
315
|
+
const matchedFields = [];
|
|
316
|
+
const snippets = [];
|
|
317
|
+
const metadataText = Object.entries(file.metadata).map(([key, value]) => `${key}: ${value}`).join("\n");
|
|
318
|
+
const metadataSnippet = firstMatchingLine(metadataText, queryLower);
|
|
319
|
+
if (metadataSnippet !== null) {
|
|
320
|
+
matchedFields.push("metadata");
|
|
321
|
+
snippets.push(metadataSnippet);
|
|
322
|
+
}
|
|
323
|
+
const bodySnippet = firstMatchingLine(file.body, queryLower);
|
|
324
|
+
if (bodySnippet !== null) {
|
|
325
|
+
matchedFields.push("body");
|
|
326
|
+
if (!snippets.includes(bodySnippet)) {
|
|
327
|
+
snippets.push(bodySnippet);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (matchedFields.length === 0) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
return { ...target, matchedFields, snippets: snippets.slice(0, 3) };
|
|
334
|
+
}
|
|
335
|
+
export async function searchClawChatMemory(root, params) {
|
|
336
|
+
const { query, queryLower, targetTypes, maxResults } = normalizeSearchParams(params);
|
|
337
|
+
const rootPath = path.resolve(root);
|
|
338
|
+
const rootRealPath = (await pathExists(rootPath)) ? await fs.realpath(rootPath) : rootPath;
|
|
339
|
+
const matches = [];
|
|
340
|
+
for (const targetType of targetTypes) {
|
|
341
|
+
const targets = await listSafeMemoryFiles(rootPath, rootRealPath, targetType);
|
|
342
|
+
for (const target of targets) {
|
|
343
|
+
const file = await readClawChatMemoryFile(rootPath, target);
|
|
344
|
+
if (!file.exists) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const match = buildSearchMatch(target, file, queryLower);
|
|
348
|
+
if (match !== null) {
|
|
349
|
+
matches.push(match);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { query, matches: matches.slice(0, maxResults), truncated: matches.length > maxResults };
|
|
354
|
+
}
|
|
355
|
+
export async function deleteClawChatMemoryFile(root, target) {
|
|
356
|
+
const targetPath = await ensureClawChatMemoryTargetSafe(root, target);
|
|
357
|
+
await fs.unlink(targetPath).catch((error) => {
|
|
358
|
+
if (error.code !== "ENOENT") {
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
export async function writeClawChatMemoryBody(root, target, mode, content) {
|
|
364
|
+
if (mode !== "append" && mode !== "replace") {
|
|
365
|
+
throw new Error("Invalid clawchat memory write mode");
|
|
366
|
+
}
|
|
367
|
+
const file = await readExistingClawChatMemoryFile(root, target);
|
|
368
|
+
let rawBody = file.parsed.rawBody;
|
|
369
|
+
if (mode === "replace") {
|
|
370
|
+
rawBody = content;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
if (content.length === 0) {
|
|
374
|
+
throw new Error("clawchat memory append content must be non-empty");
|
|
375
|
+
}
|
|
376
|
+
if (rawBody.length === 0 || rawBody.endsWith("\n") || rawBody.endsWith("\r") || content.startsWith("\n") || content.startsWith("\r")) {
|
|
377
|
+
rawBody = `${rawBody}${content}`;
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
rawBody = `${rawBody}\n${content}`;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
await writeFileAtomically(file.targetPath, formatRawMemoryFile(file.parsed, rawBody));
|
|
384
|
+
}
|
|
385
|
+
export async function editClawChatMemoryBody(root, target, oldText, newText) {
|
|
386
|
+
const normalizedOldText = normalizeLineEndings(oldText);
|
|
387
|
+
if (normalizedOldText.length === 0) {
|
|
388
|
+
throw new Error("clawchat memory edit oldText must be non-empty");
|
|
389
|
+
}
|
|
390
|
+
const file = await readExistingClawChatMemoryFile(root, target);
|
|
391
|
+
const { normalized: body, rawBoundaries } = normalizeWithRawBoundaryMap(file.parsed.rawBody);
|
|
392
|
+
const firstIndex = body.indexOf(normalizedOldText);
|
|
393
|
+
if (firstIndex === -1 || body.indexOf(normalizedOldText, firstIndex + 1) !== -1) {
|
|
394
|
+
throw new Error("clawchat memory edit requires exactly one oldText match");
|
|
395
|
+
}
|
|
396
|
+
const updatedRawBody = `${file.parsed.rawBody.slice(0, rawBoundaries[firstIndex])}${normalizeLineEndings(newText)}${file.parsed.rawBody.slice(rawBoundaries[firstIndex + normalizedOldText.length])}`;
|
|
397
|
+
await writeFileAtomically(file.targetPath, formatRawMemoryFile(file.parsed, updatedRawBody));
|
|
398
|
+
}
|
|
399
|
+
export async function writeClawChatMetadata(root, target, metadata) {
|
|
400
|
+
const file = await readExistingClawChatMemoryFile(root, target);
|
|
401
|
+
const metadataToWrite = metadataWithInitialDefaults(target, file.exists, metadata);
|
|
402
|
+
await writeFileAtomically(file.targetPath, `${formatMetadataBlock(metadataToWrite)}${rawBodySuffixForMetadataReplacement(file.parsed)}`);
|
|
403
|
+
}
|