@newbase-clawchat/openclaw-clawchat 2026.5.12-2 → 2026.5.12-21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -17
- package/dist/index.js +3 -1
- package/dist/src/api-client.js +71 -12
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +5 -5
- package/dist/src/channel.setup.js +4 -17
- package/dist/src/clawchat-memory.js +290 -0
- package/dist/src/clawchat-metadata.js +235 -0
- package/dist/src/client.js +31 -93
- package/dist/src/commands.js +3 -3
- package/dist/src/config.js +58 -3
- package/dist/src/group-message-coalescer.js +107 -0
- package/dist/src/inbound.js +24 -28
- package/dist/src/login.runtime.js +82 -19
- package/dist/src/media-runtime.js +2 -3
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +281 -56
- package/dist/src/plugin-prompts.js +76 -0
- package/dist/src/profile-prompt.js +150 -0
- package/dist/src/profile-sync.js +169 -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 +2 -2
- package/dist/src/reply-dispatcher.js +143 -40
- package/dist/src/runtime.js +813 -109
- package/dist/src/storage.js +636 -0
- package/dist/src/tools-schema.js +70 -10
- package/dist/src/tools.js +600 -112
- package/dist/src/ws-alignment.js +8 -0
- package/dist/src/ws-client.js +588 -0
- package/index.ts +6 -1
- package/openclaw.plugin.json +44 -4
- package/package.json +4 -3
- package/prompts/platform.md +7 -0
- package/skills/clawchat/SKILL.md +90 -0
- package/src/api-client.test.ts +360 -15
- package/src/api-client.ts +127 -25
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +71 -4
- package/src/buffered-stream.test.ts +1 -1
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +270 -60
- package/src/channel.setup.ts +9 -18
- package/src/channel.test.ts +33 -25
- package/src/channel.ts +5 -7
- package/src/clawchat-memory.test.ts +372 -0
- package/src/clawchat-memory.ts +363 -0
- package/src/clawchat-metadata.test.ts +350 -0
- package/src/clawchat-metadata.ts +352 -0
- package/src/client.test.ts +57 -48
- package/src/client.ts +37 -129
- package/src/commands.test.ts +2 -2
- package/src/commands.ts +3 -3
- package/src/config.test.ts +169 -4
- package/src/config.ts +86 -6
- package/src/group-message-coalescer.test.ts +223 -0
- package/src/group-message-coalescer.ts +154 -0
- package/src/inbound.test.ts +106 -19
- package/src/inbound.ts +31 -35
- package/src/login.runtime.test.ts +294 -11
- package/src/login.runtime.ts +90 -21
- package/src/manifest.test.ts +86 -14
- package/src/media-runtime.test.ts +31 -2
- package/src/media-runtime.ts +7 -10
- package/src/message-mapper.test.ts +2 -2
- package/src/message-mapper.ts +2 -2
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +811 -95
- package/src/outbound.ts +332 -65
- package/src/plugin-entry.test.ts +3 -1
- package/src/plugin-prompts.test.ts +78 -0
- package/src/plugin-prompts.ts +92 -0
- package/src/profile-prompt.test.ts +435 -0
- package/src/profile-prompt.ts +208 -0
- package/src/profile-sync.test.ts +611 -0
- package/src/profile-sync.ts +268 -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.ts +2 -2
- package/src/reply-dispatcher.test.ts +720 -135
- package/src/reply-dispatcher.ts +174 -42
- package/src/runtime.test.ts +3884 -337
- package/src/runtime.ts +956 -128
- package/src/storage.test.ts +692 -0
- package/src/storage.ts +989 -0
- package/src/streaming.test.ts +1 -1
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +115 -13
- package/src/tools.test.ts +501 -10
- package/src/tools.ts +739 -133
- package/src/ws-alignment.ts +9 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
export type ClawChatMemoryTargetType = "owner" | "user" | "group";
|
|
6
|
+
|
|
7
|
+
export interface ClawChatMemoryTarget {
|
|
8
|
+
targetType: ClawChatMemoryTargetType;
|
|
9
|
+
targetId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ClawChatMemoryFile {
|
|
13
|
+
exists: boolean;
|
|
14
|
+
metadata: Record<string, string>;
|
|
15
|
+
body: string;
|
|
16
|
+
raw: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const metadataStart = "<!-- clawchat:metadata:start -->";
|
|
20
|
+
const metadataEnd = "<!-- clawchat:metadata:end -->";
|
|
21
|
+
|
|
22
|
+
interface ParsedClawChatMemoryFile {
|
|
23
|
+
metadata: Record<string, string>;
|
|
24
|
+
body: string;
|
|
25
|
+
rawMetadataBlock: string | null;
|
|
26
|
+
rawBodyPrefix: string;
|
|
27
|
+
rawBody: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assertValidTargetId(target: ClawChatMemoryTarget): void {
|
|
31
|
+
if (target.targetType !== "owner" && target.targetType !== "user" && target.targetType !== "group") {
|
|
32
|
+
throw new Error("Invalid clawchat memory targetType");
|
|
33
|
+
}
|
|
34
|
+
if (typeof target.targetId !== "string") {
|
|
35
|
+
throw new Error("Invalid clawchat memory targetId: targetId must be a string");
|
|
36
|
+
}
|
|
37
|
+
if (target.targetId.length === 0) {
|
|
38
|
+
throw new Error("Invalid clawchat memory targetId: targetId is required");
|
|
39
|
+
}
|
|
40
|
+
if (target.targetId === "." || target.targetId === ".." || target.targetId.includes("..")) {
|
|
41
|
+
throw new Error("Invalid clawchat memory targetId");
|
|
42
|
+
}
|
|
43
|
+
if (/[\/\\\p{Cc}]/u.test(target.targetId)) {
|
|
44
|
+
throw new Error("Invalid clawchat memory targetId");
|
|
45
|
+
}
|
|
46
|
+
if (target.targetType === "owner" && target.targetId !== "owner") {
|
|
47
|
+
throw new Error("Invalid clawchat memory owner targetId: owner targetId must be owner");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveClawChatMemoryPath(root: string, target: ClawChatMemoryTarget): string {
|
|
52
|
+
assertValidTargetId(target);
|
|
53
|
+
|
|
54
|
+
if (target.targetType === "owner") {
|
|
55
|
+
return path.resolve(root, "owner.md");
|
|
56
|
+
}
|
|
57
|
+
if (target.targetType === "user") {
|
|
58
|
+
return path.resolve(root, "users", `${target.targetId}.md`);
|
|
59
|
+
}
|
|
60
|
+
return path.resolve(root, "groups", `${target.targetId}.md`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function pathExists(candidate: string): Promise<boolean> {
|
|
64
|
+
try {
|
|
65
|
+
await fs.lstat(candidate);
|
|
66
|
+
return true;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function assertInsideRoot(root: string, candidate: string): void {
|
|
76
|
+
const relative = path.relative(root, candidate);
|
|
77
|
+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Resolved clawchat memory path is outside root: ${candidate}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function assertExistingDirectorySafe(rootRealPath: string, dirPath: string): Promise<void> {
|
|
84
|
+
let stat;
|
|
85
|
+
try {
|
|
86
|
+
stat = await fs.lstat(dirPath);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
if (stat.isSymbolicLink()) {
|
|
94
|
+
throw new Error(`Unsafe clawchat memory directory symlink: ${dirPath}`);
|
|
95
|
+
}
|
|
96
|
+
if (!stat.isDirectory()) {
|
|
97
|
+
throw new Error(`Unsafe clawchat memory parent is not a directory: ${dirPath}`);
|
|
98
|
+
}
|
|
99
|
+
assertInsideRoot(rootRealPath, await fs.realpath(dirPath));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function ensureClawChatMemoryTargetSafe(root: string, target: ClawChatMemoryTarget): Promise<string> {
|
|
103
|
+
const targetPath = resolveClawChatMemoryPath(root, target);
|
|
104
|
+
const rootPath = path.resolve(root);
|
|
105
|
+
const rootRealPath = (await pathExists(rootPath)) ? await fs.realpath(rootPath) : rootPath;
|
|
106
|
+
assertInsideRoot(rootPath, targetPath);
|
|
107
|
+
|
|
108
|
+
if (target.targetType !== "owner") {
|
|
109
|
+
await assertExistingDirectorySafe(rootRealPath, path.dirname(targetPath));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let stat;
|
|
113
|
+
try {
|
|
114
|
+
stat = await fs.lstat(targetPath);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
117
|
+
return targetPath;
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
if (stat.isSymbolicLink()) {
|
|
122
|
+
throw new Error(`Unsafe clawchat memory target symlink: ${targetPath}`);
|
|
123
|
+
}
|
|
124
|
+
if (!stat.isFile()) {
|
|
125
|
+
throw new Error(`Unsafe clawchat memory target is not a regular file: ${targetPath}`);
|
|
126
|
+
}
|
|
127
|
+
assertInsideRoot(rootRealPath, await fs.realpath(targetPath));
|
|
128
|
+
return targetPath;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeLineEndings(value: string): string {
|
|
132
|
+
return value.replace(/\r\n?/g, "\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeMetadataValue(value: string): string {
|
|
136
|
+
return value.replace(/[\r\n]+/g, " ");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function consumeLineEnding(value: string, index: number): number {
|
|
140
|
+
if (value.startsWith("\r\n", index)) {
|
|
141
|
+
return 2;
|
|
142
|
+
}
|
|
143
|
+
if (value[index] === "\n" || value[index] === "\r") {
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function splitRawBodySuffix(rawBodySuffix: string): { prefix: string; rawBody: string } {
|
|
150
|
+
let offset = consumeLineEnding(rawBodySuffix, 0);
|
|
151
|
+
if (offset === 0) {
|
|
152
|
+
return { prefix: "", rawBody: rawBodySuffix };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
offset += consumeLineEnding(rawBodySuffix, offset);
|
|
156
|
+
return { prefix: rawBodySuffix.slice(0, offset), rawBody: rawBodySuffix.slice(offset) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseMetadataBlock(rawMetadataBlock: string): Record<string, string> {
|
|
160
|
+
const metadata: Record<string, string> = {};
|
|
161
|
+
const lines = normalizeLineEndings(rawMetadataBlock).split("\n");
|
|
162
|
+
for (const line of lines.slice(1, -1)) {
|
|
163
|
+
const separatorIndex = line.indexOf(":");
|
|
164
|
+
if (separatorIndex <= 0) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
168
|
+
if (key.length === 0) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
metadata[key] = line.slice(separatorIndex + 1).trimStart();
|
|
172
|
+
}
|
|
173
|
+
return metadata;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseClawChatMemoryRaw(raw: string): ParsedClawChatMemoryFile {
|
|
177
|
+
const firstLineEnd = raw.indexOf("\n");
|
|
178
|
+
const firstLineRawEnd = firstLineEnd === -1 ? raw.length : firstLineEnd;
|
|
179
|
+
const firstLineContentEnd = raw[firstLineRawEnd - 1] === "\r" ? firstLineRawEnd - 1 : firstLineRawEnd;
|
|
180
|
+
|
|
181
|
+
if (raw.slice(0, firstLineContentEnd) !== metadataStart) {
|
|
182
|
+
return { metadata: {}, body: normalizeLineEndings(raw), rawMetadataBlock: null, rawBodyPrefix: "", rawBody: raw };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let lineStart = firstLineEnd === -1 ? raw.length : firstLineEnd + 1;
|
|
186
|
+
while (lineStart < raw.length) {
|
|
187
|
+
const lineEnd = raw.indexOf("\n", lineStart);
|
|
188
|
+
const lineRawEnd = lineEnd === -1 ? raw.length : lineEnd;
|
|
189
|
+
const lineContentEnd = raw[lineRawEnd - 1] === "\r" ? lineRawEnd - 1 : lineRawEnd;
|
|
190
|
+
if (raw.slice(lineStart, lineContentEnd) === metadataEnd) {
|
|
191
|
+
const rawMetadataBlock = raw.slice(0, lineContentEnd);
|
|
192
|
+
const { prefix, rawBody } = splitRawBodySuffix(raw.slice(lineContentEnd));
|
|
193
|
+
return {
|
|
194
|
+
metadata: parseMetadataBlock(rawMetadataBlock),
|
|
195
|
+
body: normalizeLineEndings(rawBody),
|
|
196
|
+
rawMetadataBlock,
|
|
197
|
+
rawBodyPrefix: prefix,
|
|
198
|
+
rawBody,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (lineEnd === -1) {
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
lineStart = lineEnd + 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { metadata: {}, body: normalizeLineEndings(raw), rawMetadataBlock: null, rawBodyPrefix: "", rawBody: raw };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function formatMetadataBlock(metadata: Record<string, string>): string {
|
|
211
|
+
const lines = [metadataStart];
|
|
212
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
213
|
+
lines.push(`${key}: ${normalizeMetadataValue(value)}`);
|
|
214
|
+
}
|
|
215
|
+
lines.push(metadataEnd);
|
|
216
|
+
return lines.join("\n");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function formatRawMemoryFile(parsed: ParsedClawChatMemoryFile, rawBody: string): string {
|
|
220
|
+
if (parsed.rawMetadataBlock === null) {
|
|
221
|
+
return rawBody;
|
|
222
|
+
}
|
|
223
|
+
if (rawBody.length === 0) {
|
|
224
|
+
return parsed.rawMetadataBlock;
|
|
225
|
+
}
|
|
226
|
+
return `${parsed.rawMetadataBlock}${parsed.rawBodyPrefix || "\n\n"}${rawBody}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function rawBodySuffixForMetadataReplacement(parsed: ParsedClawChatMemoryFile): string {
|
|
230
|
+
if (parsed.rawMetadataBlock === null) {
|
|
231
|
+
return parsed.rawBody.length === 0 ? "" : `\n\n${parsed.rawBody}`;
|
|
232
|
+
}
|
|
233
|
+
return `${parsed.rawBodyPrefix}${parsed.rawBody}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizeWithRawBoundaryMap(raw: string): { normalized: string; rawBoundaries: number[] } {
|
|
237
|
+
let normalized = "";
|
|
238
|
+
const rawBoundaries = [0];
|
|
239
|
+
let rawIndex = 0;
|
|
240
|
+
while (rawIndex < raw.length) {
|
|
241
|
+
if (raw[rawIndex] === "\r") {
|
|
242
|
+
normalized += "\n";
|
|
243
|
+
rawIndex += raw[rawIndex + 1] === "\n" ? 2 : 1;
|
|
244
|
+
} else {
|
|
245
|
+
normalized += raw[rawIndex];
|
|
246
|
+
rawIndex += 1;
|
|
247
|
+
}
|
|
248
|
+
rawBoundaries.push(rawIndex);
|
|
249
|
+
}
|
|
250
|
+
return { normalized, rawBoundaries };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function readExistingClawChatMemoryFile(root: string, target: ClawChatMemoryTarget): Promise<{
|
|
254
|
+
targetPath: string;
|
|
255
|
+
exists: boolean;
|
|
256
|
+
parsed: ParsedClawChatMemoryFile;
|
|
257
|
+
raw: string;
|
|
258
|
+
}> {
|
|
259
|
+
const targetPath = await ensureClawChatMemoryTargetSafe(root, target);
|
|
260
|
+
let raw: string;
|
|
261
|
+
try {
|
|
262
|
+
raw = await fs.readFile(targetPath, "utf8");
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
265
|
+
return {
|
|
266
|
+
targetPath,
|
|
267
|
+
exists: false,
|
|
268
|
+
parsed: { metadata: {}, body: "", rawMetadataBlock: null, rawBodyPrefix: "", rawBody: "" },
|
|
269
|
+
raw: "",
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
return { targetPath, exists: true, parsed: parseClawChatMemoryRaw(raw), raw };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function writeFileAtomically(targetPath: string, content: string): Promise<void> {
|
|
278
|
+
const dir = path.dirname(targetPath);
|
|
279
|
+
await fs.mkdir(dir, { recursive: true });
|
|
280
|
+
const tempPath = path.join(dir, `.${path.basename(targetPath)}.${randomUUID()}.tmp`);
|
|
281
|
+
try {
|
|
282
|
+
await fs.writeFile(tempPath, content, "utf8");
|
|
283
|
+
await fs.rename(tempPath, targetPath);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function readClawChatMemoryFile(root: string, target: ClawChatMemoryTarget): Promise<ClawChatMemoryFile> {
|
|
291
|
+
const file = await readExistingClawChatMemoryFile(root, target);
|
|
292
|
+
return {
|
|
293
|
+
exists: file.exists,
|
|
294
|
+
metadata: file.parsed.metadata,
|
|
295
|
+
body: file.parsed.body,
|
|
296
|
+
raw: file.raw,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function writeClawChatMemoryBody(
|
|
301
|
+
root: string,
|
|
302
|
+
target: ClawChatMemoryTarget,
|
|
303
|
+
mode: "append" | "replace",
|
|
304
|
+
content: string,
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
if (mode !== "append" && mode !== "replace") {
|
|
307
|
+
throw new Error("Invalid clawchat memory write mode");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const file = await readExistingClawChatMemoryFile(root, target);
|
|
311
|
+
let rawBody = file.parsed.rawBody;
|
|
312
|
+
|
|
313
|
+
if (mode === "replace") {
|
|
314
|
+
rawBody = content;
|
|
315
|
+
} else {
|
|
316
|
+
if (content.length === 0) {
|
|
317
|
+
throw new Error("clawchat memory append content must be non-empty");
|
|
318
|
+
}
|
|
319
|
+
if (rawBody.length === 0 || rawBody.endsWith("\n") || rawBody.endsWith("\r") || content.startsWith("\n") || content.startsWith("\r")) {
|
|
320
|
+
rawBody = `${rawBody}${content}`;
|
|
321
|
+
} else {
|
|
322
|
+
rawBody = `${rawBody}\n${content}`;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await writeFileAtomically(file.targetPath, formatRawMemoryFile(file.parsed, rawBody));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function editClawChatMemoryBody(
|
|
330
|
+
root: string,
|
|
331
|
+
target: ClawChatMemoryTarget,
|
|
332
|
+
oldText: string,
|
|
333
|
+
newText: string,
|
|
334
|
+
): Promise<void> {
|
|
335
|
+
const normalizedOldText = normalizeLineEndings(oldText);
|
|
336
|
+
if (normalizedOldText.length === 0) {
|
|
337
|
+
throw new Error("clawchat memory edit oldText must be non-empty");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const file = await readExistingClawChatMemoryFile(root, target);
|
|
341
|
+
const { normalized: body, rawBoundaries } = normalizeWithRawBoundaryMap(file.parsed.rawBody);
|
|
342
|
+
const firstIndex = body.indexOf(normalizedOldText);
|
|
343
|
+
if (firstIndex === -1 || body.indexOf(normalizedOldText, firstIndex + 1) !== -1) {
|
|
344
|
+
throw new Error("clawchat memory edit requires exactly one oldText match");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const updatedRawBody = `${file.parsed.rawBody.slice(0, rawBoundaries[firstIndex])}${normalizeLineEndings(
|
|
348
|
+
newText,
|
|
349
|
+
)}${file.parsed.rawBody.slice(rawBoundaries[firstIndex + normalizedOldText.length])}`;
|
|
350
|
+
await writeFileAtomically(file.targetPath, formatRawMemoryFile(file.parsed, updatedRawBody));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function writeClawChatMetadata(
|
|
354
|
+
root: string,
|
|
355
|
+
target: ClawChatMemoryTarget,
|
|
356
|
+
metadata: Record<string, string>,
|
|
357
|
+
): Promise<void> {
|
|
358
|
+
const file = await readExistingClawChatMemoryFile(root, target);
|
|
359
|
+
await writeFileAtomically(
|
|
360
|
+
file.targetPath,
|
|
361
|
+
`${formatMetadataBlock(metadata)}${rawBodySuffixForMetadataReplacement(file.parsed)}`,
|
|
362
|
+
);
|
|
363
|
+
}
|