@xfxstudio/claworld 2026.4.21-testing.2 → 2026.4.22-testing.4
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/index.js +14 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/claworld-a2a-channel-agent/SKILL.md +6 -0
- package/skills/claworld-help/SKILL.md +2 -0
- package/skills/claworld-join-and-chat/SKILL.md +57 -4
- package/src/openclaw/index.js +15 -0
- package/src/openclaw/plugin/claworld-channel-plugin.js +25 -0
- package/src/openclaw/plugin/register.js +170 -0
- package/src/openclaw/runtime/product-shell-helper.js +119 -0
- package/src/openclaw/runtime/tool-contracts.js +52 -4
- package/src/openclaw/runtime/tool-inventory.js +6 -0
- package/src/openclaw/runtime/working-memory.js +702 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export const CLAWORLD_WORKING_MEMORY_DIR = '.claworld';
|
|
6
|
+
export const CLAWORLD_CONTEXT_DIR = 'context';
|
|
7
|
+
export const CLAWORLD_JOURNAL_DIR = 'journal';
|
|
8
|
+
export const CLAWORLD_REPORTS_DIR = 'reports';
|
|
9
|
+
|
|
10
|
+
export const CLAWORLD_WORKING_MEMORY_FILES = Object.freeze({
|
|
11
|
+
index: 'INDEX.md',
|
|
12
|
+
now: 'context/NOW.md',
|
|
13
|
+
profile: 'context/PROFILE.md',
|
|
14
|
+
memory: 'context/MEMORY.md',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const CLAWORLD_WORKING_MEMORY_DIRECTORIES = Object.freeze([
|
|
18
|
+
CLAWORLD_WORKING_MEMORY_DIR,
|
|
19
|
+
`${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_CONTEXT_DIR}`,
|
|
20
|
+
`${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_JOURNAL_DIR}`,
|
|
21
|
+
`${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_REPORTS_DIR}`,
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export const CLAWORLD_MAINTENANCE_RUN_TYPES = Object.freeze({
|
|
25
|
+
L1_NOW_REFRESH: 'L1_NOW_REFRESH',
|
|
26
|
+
L2_MEMORY_PROFILE_REVIEW: 'L2_MEMORY_PROFILE_REVIEW',
|
|
27
|
+
L2_PROFILE_MEMORY_REVIEW: 'L2_MEMORY_PROFILE_REVIEW',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const MAINTENANCE_RUN_TYPE_ALIASES = Object.freeze({
|
|
31
|
+
L1_NOW_REFRESH: CLAWORLD_MAINTENANCE_RUN_TYPES.L1_NOW_REFRESH,
|
|
32
|
+
L2_MEMORY_PROFILE_REVIEW: CLAWORLD_MAINTENANCE_RUN_TYPES.L2_MEMORY_PROFILE_REVIEW,
|
|
33
|
+
L2_PROFILE_MEMORY_REVIEW: CLAWORLD_MAINTENANCE_RUN_TYPES.L2_MEMORY_PROFILE_REVIEW,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const FILE_TARGET_ALIASES = Object.freeze({
|
|
37
|
+
INDEX: CLAWORLD_WORKING_MEMORY_FILES.index,
|
|
38
|
+
'INDEX.md': CLAWORLD_WORKING_MEMORY_FILES.index,
|
|
39
|
+
NOW: CLAWORLD_WORKING_MEMORY_FILES.now,
|
|
40
|
+
'NOW.md': CLAWORLD_WORKING_MEMORY_FILES.now,
|
|
41
|
+
PROFILE: CLAWORLD_WORKING_MEMORY_FILES.profile,
|
|
42
|
+
'PROFILE.md': CLAWORLD_WORKING_MEMORY_FILES.profile,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const L1_ALLOWED_TARGETS = new Set([
|
|
46
|
+
CLAWORLD_WORKING_MEMORY_FILES.now,
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const L2_ALLOWED_TARGETS = new Set([
|
|
50
|
+
CLAWORLD_WORKING_MEMORY_FILES.now,
|
|
51
|
+
CLAWORLD_WORKING_MEMORY_FILES.profile,
|
|
52
|
+
CLAWORLD_WORKING_MEMORY_FILES.memory,
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const MAX_EVENT_EXCERPT_CHARS = 600;
|
|
56
|
+
const MAX_MEMORY_SLICE_CHARS = 4000;
|
|
57
|
+
|
|
58
|
+
export function buildClaworldContextPointer() {
|
|
59
|
+
return [
|
|
60
|
+
'# Claworld Context Pointer',
|
|
61
|
+
'',
|
|
62
|
+
'Claworld working memory is available at `.claworld/INDEX.md`.',
|
|
63
|
+
'When the user asks about Claworld, worlds, A2A conversations, people met in Claworld, activity opportunities, or previous Claworld progress, read `.claworld/INDEX.md` first.',
|
|
64
|
+
'Do not load raw Claworld transcripts by default.',
|
|
65
|
+
].join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildClaworldWorkingMemoryTemplates() {
|
|
69
|
+
return {
|
|
70
|
+
[CLAWORLD_WORKING_MEMORY_FILES.index]: [
|
|
71
|
+
'# Claworld Working Memory',
|
|
72
|
+
'',
|
|
73
|
+
'This directory is the workspace-local private working memory for Claworld.',
|
|
74
|
+
'Read this file first when the user asks about Claworld, worlds, A2A conversations, people met in Claworld, activity opportunities, or previous Claworld progress.',
|
|
75
|
+
'',
|
|
76
|
+
'## Read Order',
|
|
77
|
+
'- `context/NOW.md` for current Claworld focus, active worlds, and recent progress.',
|
|
78
|
+
'- `context/MEMORY.md` for durable Claworld facts and decisions.',
|
|
79
|
+
'- `context/PROFILE.md` for user preferences and profile hints relevant to Claworld.',
|
|
80
|
+
'- `journal/YYYY-MM.md` for append-only summarized events.',
|
|
81
|
+
'- `reports/` for generated local progress reports.',
|
|
82
|
+
'',
|
|
83
|
+
'## Rules',
|
|
84
|
+
'- Do not load raw Claworld transcripts by default.',
|
|
85
|
+
'- Do not write this content into global `MEMORY.md` automatically.',
|
|
86
|
+
'- Prefer short summaries and references over raw chat history.',
|
|
87
|
+
'- `context/PROFILE.md` and `context/MEMORY.md` are updated only by L2 maintenance review.',
|
|
88
|
+
'',
|
|
89
|
+
].join('\n'),
|
|
90
|
+
[CLAWORLD_WORKING_MEMORY_FILES.now]: [
|
|
91
|
+
'# Claworld Now',
|
|
92
|
+
'',
|
|
93
|
+
'## Current Focus',
|
|
94
|
+
'- No active Claworld focus recorded yet.',
|
|
95
|
+
'',
|
|
96
|
+
'## Recent Activity',
|
|
97
|
+
'- No recent Claworld activity recorded yet.',
|
|
98
|
+
'',
|
|
99
|
+
'## Open Questions',
|
|
100
|
+
'- none',
|
|
101
|
+
'',
|
|
102
|
+
].join('\n'),
|
|
103
|
+
[CLAWORLD_WORKING_MEMORY_FILES.profile]: [
|
|
104
|
+
'# Claworld Profile',
|
|
105
|
+
'',
|
|
106
|
+
'## Stable Preferences',
|
|
107
|
+
'- No Claworld-specific preferences recorded yet.',
|
|
108
|
+
'',
|
|
109
|
+
'## People And Context',
|
|
110
|
+
'- No Claworld people context recorded yet.',
|
|
111
|
+
'',
|
|
112
|
+
].join('\n'),
|
|
113
|
+
[CLAWORLD_WORKING_MEMORY_FILES.memory]: [
|
|
114
|
+
'# Claworld Memory',
|
|
115
|
+
'',
|
|
116
|
+
'## Durable Facts',
|
|
117
|
+
'- No durable Claworld facts recorded yet.',
|
|
118
|
+
'',
|
|
119
|
+
'## Decisions',
|
|
120
|
+
'- No durable Claworld decisions recorded yet.',
|
|
121
|
+
'',
|
|
122
|
+
].join('\n'),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildClaworldWorkingMemoryFileSpecs() {
|
|
127
|
+
const templates = buildClaworldWorkingMemoryTemplates();
|
|
128
|
+
return Object.entries(templates).map(([relativePath, content]) => ({
|
|
129
|
+
relativePath: `${CLAWORLD_WORKING_MEMORY_DIR}/${relativePath}`,
|
|
130
|
+
workingMemoryRelativePath: relativePath,
|
|
131
|
+
policy: 'durable',
|
|
132
|
+
content,
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeText(value, fallback = null) {
|
|
137
|
+
if (value == null) return fallback;
|
|
138
|
+
const normalized = String(value).trim();
|
|
139
|
+
return normalized || fallback;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function expandUserPath(inputPath, homeDir = os.homedir()) {
|
|
143
|
+
const text = normalizeText(inputPath, null);
|
|
144
|
+
if (!text) return null;
|
|
145
|
+
if (text === '~') return homeDir;
|
|
146
|
+
if (text.startsWith('~/') || text.startsWith('~\\')) {
|
|
147
|
+
return path.join(homeDir, text.slice(2));
|
|
148
|
+
}
|
|
149
|
+
return text;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function resolveClaworldWorkspaceRoot(options = {}, homeDir = os.homedir()) {
|
|
153
|
+
const source = typeof options === 'string'
|
|
154
|
+
? options
|
|
155
|
+
: options?.workspaceRoot
|
|
156
|
+
?? options?.workspacePath
|
|
157
|
+
?? options?.workspaceDir
|
|
158
|
+
?? options?.workspace
|
|
159
|
+
?? options?.cwd
|
|
160
|
+
?? process.cwd();
|
|
161
|
+
return path.resolve(expandUserPath(source, homeDir) || process.cwd());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function resolveClaworldMemoryRoot(options = {}, homeDir = os.homedir()) {
|
|
165
|
+
return path.join(resolveClaworldWorkspaceRoot(options, homeDir), CLAWORLD_WORKING_MEMORY_DIR);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function readTextIfPresent(filePath) {
|
|
169
|
+
try {
|
|
170
|
+
return await fs.readFile(filePath, 'utf8');
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error && error.code === 'ENOENT') return null;
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function atomicWriteText(filePath, content, {
|
|
178
|
+
backup = true,
|
|
179
|
+
rejectEmptyOverwrite = true,
|
|
180
|
+
} = {}) {
|
|
181
|
+
const nextContent = String(content ?? '');
|
|
182
|
+
const currentContent = await readTextIfPresent(filePath);
|
|
183
|
+
if (
|
|
184
|
+
rejectEmptyOverwrite
|
|
185
|
+
&& currentContent != null
|
|
186
|
+
&& normalizeText(currentContent, null)
|
|
187
|
+
&& !normalizeText(nextContent, null)
|
|
188
|
+
) {
|
|
189
|
+
throw new Error(`Refusing to overwrite non-empty Claworld memory file with empty content: ${filePath}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
193
|
+
if (backup && currentContent != null && currentContent !== nextContent) {
|
|
194
|
+
await fs.writeFile(`${filePath}.bak`, currentContent, 'utf8');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const tempPath = path.join(
|
|
198
|
+
path.dirname(filePath),
|
|
199
|
+
`.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`,
|
|
200
|
+
);
|
|
201
|
+
await fs.writeFile(tempPath, nextContent, 'utf8');
|
|
202
|
+
await fs.rename(tempPath, filePath);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function ensureClaworldWorkingMemory(options = {}, ensureOptions = {}) {
|
|
206
|
+
const workspaceRoot = resolveClaworldWorkspaceRoot(options, ensureOptions.homeDir || os.homedir());
|
|
207
|
+
const memoryRoot = path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR);
|
|
208
|
+
const directories = CLAWORLD_WORKING_MEMORY_DIRECTORIES.map((relativePath) => ({
|
|
209
|
+
relativePath,
|
|
210
|
+
absolutePath: path.join(workspaceRoot, relativePath),
|
|
211
|
+
}));
|
|
212
|
+
const files = buildClaworldWorkingMemoryFileSpecs().map((file) => ({
|
|
213
|
+
...file,
|
|
214
|
+
absolutePath: path.join(workspaceRoot, file.relativePath),
|
|
215
|
+
}));
|
|
216
|
+
const actions = [];
|
|
217
|
+
|
|
218
|
+
if (ensureOptions.dryRun === true) {
|
|
219
|
+
for (const directory of directories) {
|
|
220
|
+
actions.push(`mkdir -p ${directory.absolutePath}`);
|
|
221
|
+
}
|
|
222
|
+
for (const file of files) {
|
|
223
|
+
actions.push(`seed ${file.absolutePath} if missing`);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
ok: true,
|
|
227
|
+
dryRun: true,
|
|
228
|
+
workspaceRoot,
|
|
229
|
+
memoryRoot,
|
|
230
|
+
directories,
|
|
231
|
+
files,
|
|
232
|
+
actions,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const directory of directories) {
|
|
237
|
+
await fs.mkdir(directory.absolutePath, { recursive: true });
|
|
238
|
+
actions.push(`ensured ${directory.absolutePath}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const file of files) {
|
|
242
|
+
const currentContent = await readTextIfPresent(file.absolutePath);
|
|
243
|
+
if (currentContent == null) {
|
|
244
|
+
await atomicWriteText(file.absolutePath, file.content, {
|
|
245
|
+
backup: false,
|
|
246
|
+
rejectEmptyOverwrite: false,
|
|
247
|
+
});
|
|
248
|
+
actions.push(`created ${file.absolutePath}`);
|
|
249
|
+
} else {
|
|
250
|
+
actions.push(`preserved ${file.absolutePath}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
ok: true,
|
|
256
|
+
dryRun: false,
|
|
257
|
+
workspaceRoot,
|
|
258
|
+
memoryRoot,
|
|
259
|
+
directories,
|
|
260
|
+
files,
|
|
261
|
+
actions,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function toIsoTimestamp(value = null) {
|
|
266
|
+
const date = value instanceof Date ? value : new Date(value || Date.now());
|
|
267
|
+
if (Number.isNaN(date.getTime())) return new Date().toISOString();
|
|
268
|
+
return date.toISOString();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function toMonthKey(timestamp) {
|
|
272
|
+
return toIsoTimestamp(timestamp).slice(0, 7);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function truncateText(value, maxChars = MAX_EVENT_EXCERPT_CHARS) {
|
|
276
|
+
const text = normalizeText(value, '');
|
|
277
|
+
if (text.length <= maxChars) return text;
|
|
278
|
+
return `${text.slice(0, Math.max(0, maxChars - 3))}...`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function flattenInline(value) {
|
|
282
|
+
return truncateText(String(value ?? '').replace(/\s+/g, ' ').trim());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function buildClaworldMaintenanceEvent(input = {}) {
|
|
286
|
+
const toolName = normalizeText(input.toolName, null);
|
|
287
|
+
const source = normalizeText(input.source, toolName ? 'claworld_tool' : 'claworld_runtime');
|
|
288
|
+
const kind = normalizeText(input.kind, toolName || 'milestone');
|
|
289
|
+
const timestamp = toIsoTimestamp(input.timestamp);
|
|
290
|
+
const summary = normalizeText(input.summary, toolName ? `${toolName} succeeded.` : 'Claworld milestone recorded.');
|
|
291
|
+
const refs = input.refs && typeof input.refs === 'object' && !Array.isArray(input.refs)
|
|
292
|
+
? Object.fromEntries(
|
|
293
|
+
Object.entries(input.refs)
|
|
294
|
+
.map(([key, value]) => [key, normalizeText(value, null)])
|
|
295
|
+
.filter(([, value]) => value != null),
|
|
296
|
+
)
|
|
297
|
+
: {};
|
|
298
|
+
return {
|
|
299
|
+
id: normalizeText(input.id, `${source}:${kind}:${timestamp}`),
|
|
300
|
+
timestamp,
|
|
301
|
+
source,
|
|
302
|
+
kind,
|
|
303
|
+
summary,
|
|
304
|
+
excerpt: truncateText(input.excerpt || ''),
|
|
305
|
+
refs,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function buildClaworldRuntimeMaintenanceEvent(input = {}) {
|
|
310
|
+
return buildClaworldMaintenanceEvent({
|
|
311
|
+
...input,
|
|
312
|
+
source: normalizeText(input.source, 'claworld_runtime'),
|
|
313
|
+
kind: normalizeText(input.kind, 'milestone'),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function parseToolResultPayload(result) {
|
|
318
|
+
if (!result || typeof result !== 'object') return null;
|
|
319
|
+
if (result.payload && typeof result.payload === 'object' && !Array.isArray(result.payload)) {
|
|
320
|
+
return result.payload;
|
|
321
|
+
}
|
|
322
|
+
const textContent = Array.isArray(result.content)
|
|
323
|
+
? result.content.find((entry) => entry?.type === 'text' && typeof entry.text === 'string')?.text
|
|
324
|
+
: null;
|
|
325
|
+
if (!textContent) return null;
|
|
326
|
+
try {
|
|
327
|
+
const parsed = JSON.parse(textContent);
|
|
328
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
329
|
+
} catch {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function compactResultPayload(payload = {}) {
|
|
335
|
+
const keys = [
|
|
336
|
+
'status',
|
|
337
|
+
'tool',
|
|
338
|
+
'accountId',
|
|
339
|
+
'worldId',
|
|
340
|
+
'displayName',
|
|
341
|
+
'chatRequestId',
|
|
342
|
+
'conversationKey',
|
|
343
|
+
'candidateId',
|
|
344
|
+
'feedbackId',
|
|
345
|
+
'nextAction',
|
|
346
|
+
'requiredAction',
|
|
347
|
+
'summary',
|
|
348
|
+
'message',
|
|
349
|
+
];
|
|
350
|
+
const compact = {};
|
|
351
|
+
for (const key of keys) {
|
|
352
|
+
const value = normalizeText(payload[key], null);
|
|
353
|
+
if (value != null) compact[key] = value;
|
|
354
|
+
}
|
|
355
|
+
return compact;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function buildClaworldToolMaintenanceEvent({
|
|
359
|
+
toolName,
|
|
360
|
+
params = {},
|
|
361
|
+
result = null,
|
|
362
|
+
timestamp = null,
|
|
363
|
+
} = {}) {
|
|
364
|
+
const normalizedToolName = normalizeText(toolName, null);
|
|
365
|
+
if (!normalizedToolName || !normalizedToolName.startsWith('claworld_')) return null;
|
|
366
|
+
const payload = parseToolResultPayload(result) || {};
|
|
367
|
+
const compactPayload = compactResultPayload(payload);
|
|
368
|
+
const refs = {
|
|
369
|
+
accountId: params.accountId || payload.accountId,
|
|
370
|
+
worldId: params.worldId || payload.worldId,
|
|
371
|
+
chatRequestId: params.chatRequestId || payload.chatRequestId,
|
|
372
|
+
conversationKey: params.conversationKey || payload.conversationKey,
|
|
373
|
+
candidateId: params.candidateId || payload.candidateId,
|
|
374
|
+
agentCode: params.agentCode || payload.agentCode,
|
|
375
|
+
};
|
|
376
|
+
return buildClaworldMaintenanceEvent({
|
|
377
|
+
source: 'claworld_tool',
|
|
378
|
+
kind: normalizedToolName,
|
|
379
|
+
toolName: normalizedToolName,
|
|
380
|
+
timestamp,
|
|
381
|
+
refs,
|
|
382
|
+
summary: `${normalizedToolName} succeeded.`,
|
|
383
|
+
excerpt: Object.keys(compactPayload).length > 0
|
|
384
|
+
? JSON.stringify(compactPayload)
|
|
385
|
+
: null,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function formatRefs(refs = {}) {
|
|
390
|
+
return Object.entries(refs)
|
|
391
|
+
.filter(([, value]) => value != null)
|
|
392
|
+
.map(([key, value]) => `${key}=${flattenInline(value)}`)
|
|
393
|
+
.join(', ');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export async function appendClaworldJournalEvent(options = {}, event = {}, appendOptions = {}) {
|
|
397
|
+
const workspaceRoot = resolveClaworldWorkspaceRoot(options, appendOptions.homeDir || os.homedir());
|
|
398
|
+
await ensureClaworldWorkingMemory(workspaceRoot, appendOptions);
|
|
399
|
+
const normalizedEvent = buildClaworldMaintenanceEvent(event);
|
|
400
|
+
const monthKey = toMonthKey(normalizedEvent.timestamp);
|
|
401
|
+
const journalPath = path.join(
|
|
402
|
+
workspaceRoot,
|
|
403
|
+
CLAWORLD_WORKING_MEMORY_DIR,
|
|
404
|
+
CLAWORLD_JOURNAL_DIR,
|
|
405
|
+
`${monthKey}.md`,
|
|
406
|
+
);
|
|
407
|
+
const currentContent = await readTextIfPresent(journalPath);
|
|
408
|
+
const refsLine = formatRefs(normalizedEvent.refs);
|
|
409
|
+
const entry = [
|
|
410
|
+
`## ${normalizedEvent.timestamp} - ${normalizedEvent.kind}`,
|
|
411
|
+
`- Source: ${normalizedEvent.source}`,
|
|
412
|
+
`- Summary: ${flattenInline(normalizedEvent.summary)}`,
|
|
413
|
+
normalizedEvent.excerpt ? `- Excerpt: ${flattenInline(normalizedEvent.excerpt)}` : null,
|
|
414
|
+
refsLine ? `- Refs: ${refsLine}` : null,
|
|
415
|
+
'',
|
|
416
|
+
].filter((line) => line != null).join('\n');
|
|
417
|
+
if (currentContent == null) {
|
|
418
|
+
await atomicWriteText(journalPath, `# Claworld Journal ${monthKey}\n\n${entry}`, {
|
|
419
|
+
backup: false,
|
|
420
|
+
rejectEmptyOverwrite: false,
|
|
421
|
+
});
|
|
422
|
+
} else {
|
|
423
|
+
await fs.appendFile(journalPath, `${currentContent.endsWith('\n') ? '' : '\n'}${entry}`, 'utf8');
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
ok: true,
|
|
427
|
+
journalPath,
|
|
428
|
+
event: normalizedEvent,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export async function readClaworldWorkingMemory(options = {}, readOptions = {}) {
|
|
433
|
+
const workspaceRoot = resolveClaworldWorkspaceRoot(options, readOptions.homeDir || os.homedir());
|
|
434
|
+
const maxCharsPerFile = Number.isInteger(readOptions.maxCharsPerFile) && readOptions.maxCharsPerFile > 0
|
|
435
|
+
? readOptions.maxCharsPerFile
|
|
436
|
+
: MAX_MEMORY_SLICE_CHARS;
|
|
437
|
+
const slices = {};
|
|
438
|
+
for (const relativePath of Object.values(CLAWORLD_WORKING_MEMORY_FILES)) {
|
|
439
|
+
const absolutePath = path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR, relativePath);
|
|
440
|
+
const content = await readTextIfPresent(absolutePath);
|
|
441
|
+
slices[relativePath] = content == null
|
|
442
|
+
? null
|
|
443
|
+
: {
|
|
444
|
+
content: content.length > maxCharsPerFile ? content.slice(0, maxCharsPerFile) : content,
|
|
445
|
+
truncated: content.length > maxCharsPerFile,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
workspaceRoot,
|
|
450
|
+
memoryRoot: path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR),
|
|
451
|
+
slices,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function normalizePatchOperation(operation) {
|
|
456
|
+
const normalized = normalizeText(operation, 'replace');
|
|
457
|
+
if (normalized === 'replace' || normalized === 'append_section' || normalized === 'no_op') {
|
|
458
|
+
return normalized;
|
|
459
|
+
}
|
|
460
|
+
throw new Error(`Unsupported Claworld maintenance patch operation: ${normalized}`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function normalizeMaintenanceRunType(runType) {
|
|
464
|
+
const normalized = normalizeText(runType, null);
|
|
465
|
+
const alias = MAINTENANCE_RUN_TYPE_ALIASES[normalized];
|
|
466
|
+
if (alias) return alias;
|
|
467
|
+
throw new Error(`Unsupported Claworld maintenance run type: ${runType}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function stripClaworldPrefix(rawTarget) {
|
|
471
|
+
let target = String(rawTarget || '').replace(/\\/g, '/').trim();
|
|
472
|
+
target = target.replace(/^\/+/, '');
|
|
473
|
+
if (target.startsWith(`${CLAWORLD_WORKING_MEMORY_DIR}/`)) {
|
|
474
|
+
target = target.slice(CLAWORLD_WORKING_MEMORY_DIR.length + 1);
|
|
475
|
+
}
|
|
476
|
+
return target;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function normalizePatchTarget(rawTarget) {
|
|
480
|
+
const stripped = stripClaworldPrefix(rawTarget);
|
|
481
|
+
const aliasTarget = FILE_TARGET_ALIASES[stripped] || null;
|
|
482
|
+
if (aliasTarget) return aliasTarget;
|
|
483
|
+
if (stripped === 'MEMORY.md') {
|
|
484
|
+
throw new Error('Global MEMORY.md is not a valid Claworld working-memory target; use context/MEMORY.md.');
|
|
485
|
+
}
|
|
486
|
+
const normalized = path.posix.normalize(stripped);
|
|
487
|
+
if (!normalized || normalized === '.' || normalized.startsWith('../') || normalized === '..' || path.posix.isAbsolute(normalized)) {
|
|
488
|
+
throw new Error(`Invalid Claworld maintenance patch target: ${rawTarget}`);
|
|
489
|
+
}
|
|
490
|
+
return normalized;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function isAllowedTarget(runType, target) {
|
|
494
|
+
if (target.startsWith(`${CLAWORLD_JOURNAL_DIR}/`) || target.startsWith(`${CLAWORLD_REPORTS_DIR}/`)) {
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
if (runType === CLAWORLD_MAINTENANCE_RUN_TYPES.L1_NOW_REFRESH) {
|
|
498
|
+
return L1_ALLOWED_TARGETS.has(target);
|
|
499
|
+
}
|
|
500
|
+
if (runType === CLAWORLD_MAINTENANCE_RUN_TYPES.L2_MEMORY_PROFILE_REVIEW) {
|
|
501
|
+
return L2_ALLOWED_TARGETS.has(target);
|
|
502
|
+
}
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function assertAllowedTarget(runType, target) {
|
|
507
|
+
if (!isAllowedTarget(runType, target)) {
|
|
508
|
+
throw new Error(`Claworld maintenance run ${runType} cannot write target ${target}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function normalizeReportPatch(report, index) {
|
|
513
|
+
const filename = normalizeText(report?.filename ?? report?.name, `report-${index + 1}.md`);
|
|
514
|
+
const normalizedFilename = path.posix.basename(filename.endsWith('.md') ? filename : `${filename}.md`);
|
|
515
|
+
return {
|
|
516
|
+
operation: 'replace',
|
|
517
|
+
target: `${CLAWORLD_REPORTS_DIR}/${normalizedFilename}`,
|
|
518
|
+
content: String(report?.md ?? report?.content ?? report?.text ?? ''),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function hasOwn(value, key) {
|
|
523
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function readPatchContent(patch, fieldName) {
|
|
527
|
+
if (hasOwn(patch, 'content')) return patch.content;
|
|
528
|
+
if (hasOwn(patch, 'md')) return patch.md;
|
|
529
|
+
if (hasOwn(patch, 'text')) return patch.text;
|
|
530
|
+
throw new Error(`Claworld maintenance FilePatch ${fieldName} requires content for replace or append_section.`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function normalizeFilePatchValue(value, target, fieldName) {
|
|
534
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
535
|
+
const operation = normalizePatchOperation(value.operation);
|
|
536
|
+
const normalized = {
|
|
537
|
+
operation,
|
|
538
|
+
target,
|
|
539
|
+
content: operation === 'no_op' ? '' : String(readPatchContent(value, fieldName) ?? ''),
|
|
540
|
+
};
|
|
541
|
+
const rationale = normalizeText(value.rationale, null);
|
|
542
|
+
if (rationale) normalized.rationale = rationale;
|
|
543
|
+
return normalized;
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
operation: 'replace',
|
|
547
|
+
target,
|
|
548
|
+
content: String(value ?? ''),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function normalizeClaworldMaintenanceOutput(runType, output = {}, options = {}) {
|
|
553
|
+
if (!output || typeof output !== 'object' || Array.isArray(output)) {
|
|
554
|
+
throw new Error('Claworld maintenance output must be an object.');
|
|
555
|
+
}
|
|
556
|
+
const normalizedRunType = normalizeMaintenanceRunType(runType);
|
|
557
|
+
const patches = [];
|
|
558
|
+
if (Object.prototype.hasOwnProperty.call(output, 'nowMd')) {
|
|
559
|
+
patches.push(normalizeFilePatchValue(output.nowMd, CLAWORLD_WORKING_MEMORY_FILES.now, 'nowMd'));
|
|
560
|
+
}
|
|
561
|
+
if (Object.prototype.hasOwnProperty.call(output, 'profileMd')) {
|
|
562
|
+
patches.push(normalizeFilePatchValue(output.profileMd, CLAWORLD_WORKING_MEMORY_FILES.profile, 'profileMd'));
|
|
563
|
+
}
|
|
564
|
+
if (Object.prototype.hasOwnProperty.call(output, 'memoryMd')) {
|
|
565
|
+
patches.push(normalizeFilePatchValue(output.memoryMd, CLAWORLD_WORKING_MEMORY_FILES.memory, 'memoryMd'));
|
|
566
|
+
}
|
|
567
|
+
if (Object.prototype.hasOwnProperty.call(output, 'journalAppendMd')) {
|
|
568
|
+
const monthKey = toMonthKey(options.timestamp || output.timestamp || Date.now());
|
|
569
|
+
patches.push({
|
|
570
|
+
operation: 'append_section',
|
|
571
|
+
target: `${CLAWORLD_JOURNAL_DIR}/${monthKey}.md`,
|
|
572
|
+
content: String(output.journalAppendMd ?? ''),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
if (Array.isArray(output.reports)) {
|
|
576
|
+
output.reports.forEach((report, index) => {
|
|
577
|
+
patches.push(normalizeReportPatch(report, index));
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
if (Array.isArray(output.patches)) {
|
|
581
|
+
for (const patch of output.patches) {
|
|
582
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
583
|
+
throw new Error('Claworld maintenance patches entries must be FilePatch objects.');
|
|
584
|
+
}
|
|
585
|
+
const operation = normalizePatchOperation(patch.operation);
|
|
586
|
+
patches.push({
|
|
587
|
+
operation,
|
|
588
|
+
target: normalizePatchTarget(patch.target ?? patch.path ?? patch.relativePath),
|
|
589
|
+
content: operation === 'no_op' ? '' : String(readPatchContent(patch, 'patches[]') ?? ''),
|
|
590
|
+
rationale: normalizeText(patch.rationale, null),
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const normalizedPatches = patches.map((patch) => {
|
|
596
|
+
const target = normalizePatchTarget(patch.target);
|
|
597
|
+
const operation = normalizePatchOperation(patch.operation);
|
|
598
|
+
assertAllowedTarget(normalizedRunType, target);
|
|
599
|
+
const normalizedPatch = {
|
|
600
|
+
operation,
|
|
601
|
+
target,
|
|
602
|
+
content: String(patch.content ?? ''),
|
|
603
|
+
};
|
|
604
|
+
const rationale = normalizeText(patch.rationale, null);
|
|
605
|
+
if (rationale) normalizedPatch.rationale = rationale;
|
|
606
|
+
return normalizedPatch;
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
return {
|
|
610
|
+
runType: normalizedRunType,
|
|
611
|
+
noOpReason: normalizeText(output.noOpReason, null),
|
|
612
|
+
patches: normalizedPatches,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function validateClaworldMaintenanceOutput(runType, output = {}, options = {}) {
|
|
617
|
+
return normalizeClaworldMaintenanceOutput(runType, output, options);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function applyMaintenancePatch(workspaceRoot, patch) {
|
|
621
|
+
if (patch.operation === 'no_op') {
|
|
622
|
+
return {
|
|
623
|
+
operation: patch.operation,
|
|
624
|
+
target: patch.target,
|
|
625
|
+
applied: false,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const absolutePath = path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR, patch.target);
|
|
629
|
+
if (patch.operation === 'append_section') {
|
|
630
|
+
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
631
|
+
const currentContent = await readTextIfPresent(absolutePath);
|
|
632
|
+
if (currentContent == null) {
|
|
633
|
+
const monthKey = path.basename(patch.target, '.md');
|
|
634
|
+
await atomicWriteText(absolutePath, `# Claworld Journal ${monthKey}\n\n${patch.content}`, {
|
|
635
|
+
backup: false,
|
|
636
|
+
rejectEmptyOverwrite: false,
|
|
637
|
+
});
|
|
638
|
+
} else {
|
|
639
|
+
await fs.appendFile(
|
|
640
|
+
absolutePath,
|
|
641
|
+
`${currentContent.endsWith('\n') ? '' : '\n'}${patch.content}`,
|
|
642
|
+
'utf8',
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
operation: patch.operation,
|
|
647
|
+
target: patch.target,
|
|
648
|
+
applied: true,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
await atomicWriteText(absolutePath, patch.content, {
|
|
653
|
+
backup: true,
|
|
654
|
+
rejectEmptyOverwrite: true,
|
|
655
|
+
});
|
|
656
|
+
return {
|
|
657
|
+
operation: patch.operation,
|
|
658
|
+
target: patch.target,
|
|
659
|
+
applied: true,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export async function runClaworldMemoryMaintenance(runType, requestBundle = {}, options = {}) {
|
|
664
|
+
const normalizedRunType = normalizeMaintenanceRunType(runType);
|
|
665
|
+
const workspaceRoot = resolveClaworldWorkspaceRoot(
|
|
666
|
+
options.workspaceRoot || requestBundle.workspaceRoot || requestBundle.workspaceDir || process.cwd(),
|
|
667
|
+
options.homeDir || os.homedir(),
|
|
668
|
+
);
|
|
669
|
+
await ensureClaworldWorkingMemory(workspaceRoot, options);
|
|
670
|
+
const workingMemory = await readClaworldWorkingMemory(workspaceRoot, options);
|
|
671
|
+
const request = {
|
|
672
|
+
...requestBundle,
|
|
673
|
+
runType: normalizedRunType,
|
|
674
|
+
workspaceRoot,
|
|
675
|
+
workingMemory,
|
|
676
|
+
};
|
|
677
|
+
const output = options.output
|
|
678
|
+
?? (typeof options.maintenanceRunner === 'function'
|
|
679
|
+
? await options.maintenanceRunner(request)
|
|
680
|
+
: null);
|
|
681
|
+
if (!output) {
|
|
682
|
+
return {
|
|
683
|
+
ok: true,
|
|
684
|
+
runType: normalizedRunType,
|
|
685
|
+
noOpReason: 'no_maintenance_runner',
|
|
686
|
+
request,
|
|
687
|
+
applied: [],
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const normalized = validateClaworldMaintenanceOutput(normalizedRunType, output, options);
|
|
691
|
+
const applied = [];
|
|
692
|
+
for (const patch of normalized.patches) {
|
|
693
|
+
applied.push(await applyMaintenancePatch(workspaceRoot, patch));
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
ok: true,
|
|
697
|
+
runType: normalizedRunType,
|
|
698
|
+
noOpReason: normalized.noOpReason,
|
|
699
|
+
request,
|
|
700
|
+
applied,
|
|
701
|
+
};
|
|
702
|
+
}
|