openclaw-memory-mapper 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -0
- package/bin/cli.js +97 -0
- package/dist/assets/index-KpCnSdTn.js +351 -0
- package/dist/assets/index-Lq-0Ml6V.css +1 -0
- package/dist/favicon.svg +21 -0
- package/dist/index.html +16 -0
- package/dist/server/index.js +461 -0
- package/package.json +85 -0
- package/server/index.ts +541 -0
- package/server/tsconfig.json +13 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { readdir, readFile, stat } from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { parse as csvParse } from 'csv-parse/sync';
|
|
8
|
+
const { join, relative, extname, basename, dirname } = path;
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const app = express();
|
|
12
|
+
const PORT = process.env.PORT || 3001;
|
|
13
|
+
// Parse JSON bodies
|
|
14
|
+
app.use(express.json());
|
|
15
|
+
// Serve static files from the built frontend in production
|
|
16
|
+
// When compiled: server is at dist/server/index.js, frontend at dist/
|
|
17
|
+
// When running from source: server is at server/index.ts, frontend at dist/
|
|
18
|
+
const isCompiledServer = __dirname.includes('dist/server') || __dirname.includes('dist\\server');
|
|
19
|
+
const distPath = isCompiledServer
|
|
20
|
+
? join(__dirname, '..') // dist/server -> dist/
|
|
21
|
+
: join(__dirname, '..', 'dist'); // server/ -> dist/
|
|
22
|
+
if (existsSync(join(distPath, 'index.html'))) {
|
|
23
|
+
app.use(express.static(distPath));
|
|
24
|
+
}
|
|
25
|
+
// Default memory paths (can be overridden via /api/config/sources)
|
|
26
|
+
const OPENCLAW_PATH = join(homedir(), '.openclaw');
|
|
27
|
+
let configuredSources = [];
|
|
28
|
+
// Helper to expand ~ and validate path is within home directory
|
|
29
|
+
function expandAndValidatePath(pathParam) {
|
|
30
|
+
const expanded = pathParam.startsWith('~')
|
|
31
|
+
? join(homedir(), pathParam.slice(1))
|
|
32
|
+
: pathParam;
|
|
33
|
+
const resolved = path.resolve(expanded);
|
|
34
|
+
const home = homedir();
|
|
35
|
+
if (!resolved.startsWith(home + path.sep) && resolved !== home) {
|
|
36
|
+
return { valid: false, expanded, error: 'Path must be within home directory' };
|
|
37
|
+
}
|
|
38
|
+
return { valid: true, expanded };
|
|
39
|
+
}
|
|
40
|
+
// Get base path for a source (uses configured sources if available, falls back to defaults)
|
|
41
|
+
function getBasePath(sourceId) {
|
|
42
|
+
const configured = configuredSources.find(s => s.id === sourceId);
|
|
43
|
+
if (configured) {
|
|
44
|
+
const { valid, expanded } = expandAndValidatePath(configured.path);
|
|
45
|
+
if (valid)
|
|
46
|
+
return expanded;
|
|
47
|
+
}
|
|
48
|
+
// Fallback to default
|
|
49
|
+
if (sourceId === 'openclaw')
|
|
50
|
+
return OPENCLAW_PATH;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// Security: sanitize error messages to avoid leaking sensitive info in production
|
|
54
|
+
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
|
|
55
|
+
function sanitizeError(error) {
|
|
56
|
+
if (!IS_PRODUCTION) {
|
|
57
|
+
return String(error);
|
|
58
|
+
}
|
|
59
|
+
// In production, only return generic message - log full error server-side
|
|
60
|
+
console.error('Server error:', error);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
// Recursively read directory structure
|
|
64
|
+
async function readDirRecursive(dirPath, basePath) {
|
|
65
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
66
|
+
const nodes = [];
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const fullPath = join(dirPath, entry.name);
|
|
69
|
+
const relativePath = relative(basePath, fullPath);
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
// Skip hidden directories and node_modules
|
|
72
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
73
|
+
continue;
|
|
74
|
+
const children = await readDirRecursive(fullPath, basePath);
|
|
75
|
+
nodes.push({
|
|
76
|
+
name: entry.name,
|
|
77
|
+
path: relativePath,
|
|
78
|
+
type: 'directory',
|
|
79
|
+
children,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const stats = await stat(fullPath);
|
|
84
|
+
nodes.push({
|
|
85
|
+
name: entry.name,
|
|
86
|
+
path: relativePath,
|
|
87
|
+
type: 'file',
|
|
88
|
+
size: stats.size,
|
|
89
|
+
modified: stats.mtime.toISOString(),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Sort: directories first, then alphabetically
|
|
94
|
+
return nodes.sort((a, b) => {
|
|
95
|
+
if (a.type !== b.type)
|
|
96
|
+
return a.type === 'directory' ? -1 : 1;
|
|
97
|
+
return a.name.localeCompare(b.name);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// Count files in directory tree
|
|
101
|
+
function countFiles(nodes) {
|
|
102
|
+
let count = 0;
|
|
103
|
+
for (const node of nodes) {
|
|
104
|
+
if (node.type === 'file')
|
|
105
|
+
count++;
|
|
106
|
+
if (node.children)
|
|
107
|
+
count += countFiles(node.children);
|
|
108
|
+
}
|
|
109
|
+
return count;
|
|
110
|
+
}
|
|
111
|
+
// Get total size of directory tree
|
|
112
|
+
function totalSize(nodes) {
|
|
113
|
+
let size = 0;
|
|
114
|
+
for (const node of nodes) {
|
|
115
|
+
if (node.type === 'file' && node.size)
|
|
116
|
+
size += node.size;
|
|
117
|
+
if (node.children)
|
|
118
|
+
size += totalSize(node.children);
|
|
119
|
+
}
|
|
120
|
+
return size;
|
|
121
|
+
}
|
|
122
|
+
// API: Configure source paths
|
|
123
|
+
app.post('/api/config/sources', (req, res) => {
|
|
124
|
+
const { sources } = req.body;
|
|
125
|
+
if (!Array.isArray(sources)) {
|
|
126
|
+
res.status(400).json({ error: 'sources must be an array' });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Validate all paths
|
|
130
|
+
const validated = [];
|
|
131
|
+
for (const source of sources) {
|
|
132
|
+
if (!source.id || !source.path)
|
|
133
|
+
continue;
|
|
134
|
+
const { valid, expanded, error } = expandAndValidatePath(source.path);
|
|
135
|
+
if (!valid) {
|
|
136
|
+
res.status(400).json({ error: `Invalid path for ${source.id}: ${error}` });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
validated.push({ id: source.id, path: expanded });
|
|
140
|
+
}
|
|
141
|
+
configuredSources = validated;
|
|
142
|
+
res.json({ success: true, sources: validated });
|
|
143
|
+
});
|
|
144
|
+
// API: List all files in memory directories
|
|
145
|
+
app.get('/api/files', async (_req, res) => {
|
|
146
|
+
try {
|
|
147
|
+
const result = {};
|
|
148
|
+
// Use configured sources, or fall back to default
|
|
149
|
+
const sourcesToScan = configuredSources.length > 0
|
|
150
|
+
? configuredSources
|
|
151
|
+
: [
|
|
152
|
+
{ id: 'openclaw', path: OPENCLAW_PATH },
|
|
153
|
+
];
|
|
154
|
+
for (const source of sourcesToScan) {
|
|
155
|
+
try {
|
|
156
|
+
const basePath = getBasePath(source.id) || source.path;
|
|
157
|
+
const tree = await readDirRecursive(basePath, basePath);
|
|
158
|
+
result[source.id] = {
|
|
159
|
+
tree,
|
|
160
|
+
fileCount: countFiles(tree),
|
|
161
|
+
totalSize: totalSize(tree),
|
|
162
|
+
path: basePath,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Directory doesn't exist or not accessible
|
|
167
|
+
result[source.id] = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
res.json(result);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
res.status(500).json({ error: 'Failed to read files', details: sanitizeError(error) });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
// API: Read a specific file
|
|
177
|
+
app.get('/api/file', async (req, res) => {
|
|
178
|
+
try {
|
|
179
|
+
const { path: filePath, source } = req.query;
|
|
180
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
181
|
+
res.status(400).json({ error: 'Missing path parameter' });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!source || typeof source !== 'string') {
|
|
185
|
+
res.status(400).json({ error: 'Missing source parameter' });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Get base path from configured sources or defaults
|
|
189
|
+
const basePath = getBasePath(source);
|
|
190
|
+
if (!basePath) {
|
|
191
|
+
res.status(400).json({ error: `Unknown source: ${source}` });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const resolvedBase = path.resolve(basePath);
|
|
195
|
+
const fullPath = path.resolve(basePath, filePath);
|
|
196
|
+
// Security: ensure path is within the source directory
|
|
197
|
+
if (!fullPath.startsWith(resolvedBase + path.sep) && fullPath !== resolvedBase) {
|
|
198
|
+
res.status(403).json({ error: 'Access denied' });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
202
|
+
const stats = await stat(fullPath);
|
|
203
|
+
const ext = extname(filePath).toLowerCase();
|
|
204
|
+
res.json({
|
|
205
|
+
path: filePath,
|
|
206
|
+
name: basename(filePath),
|
|
207
|
+
content,
|
|
208
|
+
size: stats.size,
|
|
209
|
+
modified: stats.mtime.toISOString(),
|
|
210
|
+
extension: ext,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (error.code === 'ENOENT') {
|
|
215
|
+
res.status(404).json({ error: 'File not found' });
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
res.status(500).json({ error: 'Failed to read file', details: sanitizeError(error) });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
// API: Get openclaw config
|
|
223
|
+
app.get('/api/config', async (_req, res) => {
|
|
224
|
+
try {
|
|
225
|
+
const configPath = join(OPENCLAW_PATH, 'openclaw.json');
|
|
226
|
+
const content = await readFile(configPath, 'utf-8');
|
|
227
|
+
const config = JSON.parse(content);
|
|
228
|
+
res.json({ config, path: configPath });
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (error.code === 'ENOENT') {
|
|
232
|
+
res.status(404).json({ error: 'Config file not found', path: join(OPENCLAW_PATH, 'openclaw.json') });
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
res.status(500).json({ error: 'Failed to read config', details: sanitizeError(error) });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
// API: Get agent logs from memory-swarm/logs
|
|
240
|
+
app.get('/api/logs', async (_req, res) => {
|
|
241
|
+
try {
|
|
242
|
+
const logsPath = join(OPENCLAW_PATH, 'memory-swarm', 'logs');
|
|
243
|
+
const files = await readdir(logsPath);
|
|
244
|
+
const csvFiles = files.filter(f => f.endsWith('.csv')).sort().reverse();
|
|
245
|
+
const logs = [];
|
|
246
|
+
// Read up to 5 most recent log files
|
|
247
|
+
for (const file of csvFiles.slice(0, 5)) {
|
|
248
|
+
try {
|
|
249
|
+
const content = await readFile(join(logsPath, file), 'utf-8');
|
|
250
|
+
const records = csvParse(content, {
|
|
251
|
+
columns: true,
|
|
252
|
+
skip_empty_lines: true,
|
|
253
|
+
relax_column_count: true,
|
|
254
|
+
});
|
|
255
|
+
logs.push({ file, entries: records });
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Skip files that can't be parsed
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
res.json({ logs, path: logsPath });
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
if (error.code === 'ENOENT') {
|
|
265
|
+
res.status(404).json({ error: 'Logs directory not found', path: join(OPENCLAW_PATH, 'memory-swarm', 'logs') });
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
res.status(500).json({ error: 'Failed to read logs', details: sanitizeError(error) });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
// API: Get memory layer stats
|
|
273
|
+
app.get('/api/stats', async (_req, res) => {
|
|
274
|
+
try {
|
|
275
|
+
const stats = {
|
|
276
|
+
layers: {
|
|
277
|
+
session: { path: 'memory/session', files: 0, size: 0, cloud: false },
|
|
278
|
+
daily: { path: 'memory', files: 0, size: 0, cloud: false },
|
|
279
|
+
supermemory: { path: 'supermemory', files: 0, size: 0, cloud: false },
|
|
280
|
+
qmd: { path: 'memory-swarm', files: 0, size: 0, cloud: false },
|
|
281
|
+
config: { path: '.openclaw', files: 0, size: 0, cloud: false },
|
|
282
|
+
},
|
|
283
|
+
connected: {
|
|
284
|
+
openclaw: false,
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
// Check openclaw directories
|
|
288
|
+
try {
|
|
289
|
+
await stat(OPENCLAW_PATH);
|
|
290
|
+
stats.connected.openclaw = true;
|
|
291
|
+
// Daily logs: count .md files directly in ~/.openclaw/memory/
|
|
292
|
+
try {
|
|
293
|
+
const memoryPath = join(OPENCLAW_PATH, 'memory');
|
|
294
|
+
const entries = await readdir(memoryPath, { withFileTypes: true });
|
|
295
|
+
const mdFiles = entries.filter(e => e.isFile() && e.name.endsWith('.md'));
|
|
296
|
+
let totalMdSize = 0;
|
|
297
|
+
for (const file of mdFiles) {
|
|
298
|
+
const fileStat = await stat(join(memoryPath, file.name));
|
|
299
|
+
totalMdSize += fileStat.size;
|
|
300
|
+
}
|
|
301
|
+
stats.layers.daily.files = mdFiles.length;
|
|
302
|
+
stats.layers.daily.size = totalMdSize;
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// memory directory doesn't exist
|
|
306
|
+
}
|
|
307
|
+
// QMD Swarm
|
|
308
|
+
try {
|
|
309
|
+
const qmdPath = join(OPENCLAW_PATH, 'memory-swarm');
|
|
310
|
+
const tree = await readDirRecursive(qmdPath, qmdPath);
|
|
311
|
+
stats.layers.qmd.files = countFiles(tree);
|
|
312
|
+
stats.layers.qmd.size = totalSize(tree);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// memory-swarm doesn't exist
|
|
316
|
+
}
|
|
317
|
+
// Config files
|
|
318
|
+
const configTree = await readDirRecursive(OPENCLAW_PATH, OPENCLAW_PATH);
|
|
319
|
+
stats.layers.config.files = countFiles(configTree);
|
|
320
|
+
stats.layers.config.size = totalSize(configTree);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// openclaw not accessible
|
|
324
|
+
}
|
|
325
|
+
res.json(stats);
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
res.status(500).json({ error: 'Failed to get stats', details: sanitizeError(error) });
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
// Health check
|
|
332
|
+
app.get('/api/health', (_req, res) => {
|
|
333
|
+
const openclawExists = existsSync(OPENCLAW_PATH);
|
|
334
|
+
res.json({
|
|
335
|
+
status: 'ok',
|
|
336
|
+
timestamp: new Date().toISOString(),
|
|
337
|
+
openclaw: openclawExists,
|
|
338
|
+
paths: {
|
|
339
|
+
openclaw: OPENCLAW_PATH,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
// Check if a path exists
|
|
344
|
+
// Security: only allows checking if directories exist, not reading contents
|
|
345
|
+
// Path must be within user's home directory
|
|
346
|
+
app.get('/api/check-path', (req, res) => {
|
|
347
|
+
const pathParam = req.query.path;
|
|
348
|
+
if (!pathParam) {
|
|
349
|
+
res.status(400).json({ error: 'Path parameter required' });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Expand ~ to home directory
|
|
353
|
+
const expandedPath = pathParam.startsWith('~')
|
|
354
|
+
? join(homedir(), pathParam.slice(1))
|
|
355
|
+
: pathParam;
|
|
356
|
+
// Security: only allow checking paths within user's home directory
|
|
357
|
+
const resolvedPath = path.resolve(expandedPath);
|
|
358
|
+
const home = homedir();
|
|
359
|
+
if (!resolvedPath.startsWith(home + path.sep) && resolvedPath !== home) {
|
|
360
|
+
res.status(403).json({ error: 'Access denied: path must be within home directory' });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const exists = existsSync(expandedPath);
|
|
364
|
+
res.json({
|
|
365
|
+
path: pathParam,
|
|
366
|
+
expandedPath,
|
|
367
|
+
exists,
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
const layerDocConfigs = {
|
|
371
|
+
input: {
|
|
372
|
+
title: 'User Input',
|
|
373
|
+
summary: 'Your question or message to the AI, analyzed to determine context needs.',
|
|
374
|
+
details: 'Every conversation starts here. Your input is analyzed to identify what memories and context might be relevant. The system looks for keywords, topics, and patterns that match stored information across all memory layers.',
|
|
375
|
+
updateFrequency: 'Real-time',
|
|
376
|
+
components: ['Query parsing', 'Intent detection', 'Topic extraction', 'Context triggers'],
|
|
377
|
+
docFiles: [],
|
|
378
|
+
},
|
|
379
|
+
session: {
|
|
380
|
+
title: 'Session Context',
|
|
381
|
+
summary: 'Current conversation held in working memory during your session.',
|
|
382
|
+
details: 'Maintains immediate conversation context and working memory. This includes what you\'ve discussed, recent tool usage patterns, active tasks, and context switches. Cleared on restart but persists within a session for continuity.',
|
|
383
|
+
updateFrequency: 'Real-time during session',
|
|
384
|
+
components: ['Current conversation thread', 'Recent tool usage', 'Active task tracking', 'Context switches'],
|
|
385
|
+
docFiles: ['memory/CONTEXT_BUILDER.md'],
|
|
386
|
+
},
|
|
387
|
+
daily: {
|
|
388
|
+
title: 'Daily Logs',
|
|
389
|
+
summary: 'Markdown files capturing insights and summaries from each day.',
|
|
390
|
+
details: 'Written at session end, these logs capture important decisions, preferences learned, and context from each day. They bridge sessions so the AI remembers yesterday\'s discussions and decisions without needing to reload everything.',
|
|
391
|
+
updateFrequency: 'Daily (end of session)',
|
|
392
|
+
components: ['Session summaries', 'Decisions made', 'Preferences learned', 'Important context'],
|
|
393
|
+
docFiles: ['memory/PERSISTENT_MEMORY.md', 'MEMORY.md'],
|
|
394
|
+
},
|
|
395
|
+
supermemory: {
|
|
396
|
+
title: 'SuperMemory',
|
|
397
|
+
summary: 'Cloud-based semantic search over long-term memories.',
|
|
398
|
+
details: 'A cloud service that provides semantic search over your entire memory history. When you ask about past projects, preferences, or decisions from weeks or months ago, SuperMemory finds relevant context using vector embeddings and similarity search.',
|
|
399
|
+
updateFrequency: 'Continuous (cloud sync)',
|
|
400
|
+
components: ['Vector embeddings', 'Semantic search', 'Long-term storage', 'Cross-session recall'],
|
|
401
|
+
docFiles: ['memory/ADVANCED_MEMORY_ARCHITECTURE.md'],
|
|
402
|
+
},
|
|
403
|
+
qmd: {
|
|
404
|
+
title: 'QMD Swarm',
|
|
405
|
+
summary: 'Multi-agent system that orchestrates memory retrieval and synthesis.',
|
|
406
|
+
details: 'The Query-Memory-Dispatch system coordinates retrieval from all memory layers. It runs on every query, combining results from session context, daily logs, and SuperMemory into a coherent context package that informs the response.',
|
|
407
|
+
updateFrequency: 'Every query',
|
|
408
|
+
components: ['Query analysis', 'Parallel retrieval', 'Result ranking', 'Context synthesis'],
|
|
409
|
+
docFiles: ['AGENTS.md', 'memory/ADVANCED_MEMORY_ARCHITECTURE.md'],
|
|
410
|
+
},
|
|
411
|
+
output: {
|
|
412
|
+
title: 'Response',
|
|
413
|
+
summary: 'AI\'s answer enriched with retrieved memories and full context.',
|
|
414
|
+
details: 'The final response is generated after gathering context from all memory layers. It\'s personalized based on your history, preferences, and the specific context retrieved for your query.',
|
|
415
|
+
updateFrequency: 'Per response',
|
|
416
|
+
components: ['Context integration', 'Personalization', 'Response generation', 'Memory-informed answers'],
|
|
417
|
+
docFiles: [],
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
// API: Get layer documentation
|
|
421
|
+
app.get('/api/layer-docs/:layer', async (req, res) => {
|
|
422
|
+
const layer = req.params.layer;
|
|
423
|
+
const config = layerDocConfigs[layer];
|
|
424
|
+
if (!config) {
|
|
425
|
+
res.status(404).json({ error: 'Layer not found' });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
// Try to read additional content from doc files
|
|
429
|
+
let additionalContent = '';
|
|
430
|
+
const sourceFiles = [];
|
|
431
|
+
for (const docFile of config.docFiles) {
|
|
432
|
+
try {
|
|
433
|
+
const fullPath = join(OPENCLAW_PATH, docFile);
|
|
434
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
435
|
+
additionalContent += `\n\n---\n\n**From ${docFile}:**\n\n${content.slice(0, 2000)}${content.length > 2000 ? '...' : ''}`;
|
|
436
|
+
sourceFiles.push(docFile);
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
// File not found, skip
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
res.json({
|
|
443
|
+
layer,
|
|
444
|
+
...config,
|
|
445
|
+
additionalContent: additionalContent || null,
|
|
446
|
+
sourceFiles,
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
// Catch-all: serve index.html for client-side routing (production only)
|
|
450
|
+
const indexPath = join(distPath, 'index.html');
|
|
451
|
+
if (existsSync(indexPath)) {
|
|
452
|
+
app.get('{*path}', (_req, res) => {
|
|
453
|
+
res.sendFile(indexPath);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
// Bind to localhost only to prevent network exposure
|
|
457
|
+
const HOST = '127.0.0.1';
|
|
458
|
+
app.listen(Number(PORT), HOST, () => {
|
|
459
|
+
console.log(`OpenClaw Memory Mapper API server running on http://${HOST}:${PORT}`);
|
|
460
|
+
console.log(` OPENCLAW_PATH: ${OPENCLAW_PATH}`);
|
|
461
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-memory-mapper",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Visualize your Claude agent's memory architecture",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openclaw-memory-mapper": "bin/cli.js",
|
|
8
|
+
"memory-mapper": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"dist",
|
|
13
|
+
"server"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node bin/cli.js",
|
|
17
|
+
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
|
|
18
|
+
"dev:client": "vite",
|
|
19
|
+
"dev:server": "tsx watch server/index.ts",
|
|
20
|
+
"build": "npm run build:client && npm run build:server",
|
|
21
|
+
"build:client": "vite build",
|
|
22
|
+
"build:server": "tsc -p tsconfig.server.json",
|
|
23
|
+
"lint": "eslint .",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"preview": "vite preview",
|
|
26
|
+
"prepare": "husky",
|
|
27
|
+
"prepublishOnly": "npm run build",
|
|
28
|
+
"postinstall": "rm -rf node_modules/eslint-plugin-react-hooks/node_modules/zod-validation-error"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"csv-parse": "^5.6.0",
|
|
32
|
+
"express": "^5.2.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@commitlint/cli": "^20.3.1",
|
|
36
|
+
"@commitlint/config-conventional": "^20.3.1",
|
|
37
|
+
"@eslint/js": "^9.39.2",
|
|
38
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
39
|
+
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
40
|
+
"@radix-ui/react-separator": "^1.1.8",
|
|
41
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
42
|
+
"@radix-ui/react-tabs": "^1.1.13",
|
|
43
|
+
"@radix-ui/react-toast": "^1.2.15",
|
|
44
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
45
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
46
|
+
"@semantic-release/git": "^10.0.1",
|
|
47
|
+
"@semantic-release/github": "^12.0.2",
|
|
48
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
49
|
+
"@types/express": "^5.0.6",
|
|
50
|
+
"@types/node": "^24.10.9",
|
|
51
|
+
"@types/react": "^19.2.10",
|
|
52
|
+
"@types/react-dom": "^19.2.3",
|
|
53
|
+
"@vitejs/plugin-react": "^5.1.2",
|
|
54
|
+
"@xyflow/react": "^12.10.0",
|
|
55
|
+
"autoprefixer": "^10.4.23",
|
|
56
|
+
"class-variance-authority": "^0.7.1",
|
|
57
|
+
"clsx": "^2.1.1",
|
|
58
|
+
"commitlint": "^20.3.1",
|
|
59
|
+
"concurrently": "^9.2.1",
|
|
60
|
+
"eslint": "^9.39.2",
|
|
61
|
+
"eslint-plugin-react-hooks": "7.0.1",
|
|
62
|
+
"eslint-plugin-react-refresh": "^0.4.26",
|
|
63
|
+
"globals": "^16.5.0",
|
|
64
|
+
"husky": "^9.1.7",
|
|
65
|
+
"lucide-react": "^0.563.0",
|
|
66
|
+
"postcss": "^8.5.6",
|
|
67
|
+
"react": "^19.2.4",
|
|
68
|
+
"react-dom": "^19.2.4",
|
|
69
|
+
"semantic-release": "^25.0.2",
|
|
70
|
+
"tailwind-merge": "^3.4.0",
|
|
71
|
+
"tailwindcss": "^4.1.18",
|
|
72
|
+
"tsx": "^4.21.0",
|
|
73
|
+
"typescript": "~5.9.3",
|
|
74
|
+
"typescript-eslint": "^8.54.0",
|
|
75
|
+
"vite": "^6.3.0",
|
|
76
|
+
"zod-validation-error": "4",
|
|
77
|
+
"zustand": "^5.0.10"
|
|
78
|
+
},
|
|
79
|
+
"overrides": {
|
|
80
|
+
"zod-validation-error": "4"
|
|
81
|
+
},
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public"
|
|
84
|
+
}
|
|
85
|
+
}
|