@sym-bot/sym 0.1.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/.claude/settings.local.json +10 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +12 -0
- package/LICENSE +200 -0
- package/PRD.md +173 -0
- package/README.md +231 -0
- package/TECHNICAL-SPEC.md +335 -0
- package/bin/setup-claude.sh +55 -0
- package/integrations/claude-code/mcp-server-minimal.js +51 -0
- package/integrations/claude-code/mcp-server.js +142 -0
- package/lib/claude-memory-bridge.js +217 -0
- package/lib/config.js +50 -0
- package/lib/context-encoder.js +62 -0
- package/lib/frame-parser.js +59 -0
- package/lib/memory-store.js +97 -0
- package/lib/node.js +507 -0
- package/package.json +38 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ClaudeMemoryBridge — connects Claude Code's memory system to the SYM mesh.
|
|
9
|
+
*
|
|
10
|
+
* Watches Claude Code's project memory directory for changes.
|
|
11
|
+
* When Claude Code saves a memory, the bridge:
|
|
12
|
+
* 1. Re-encodes the node's cognitive state from all Claude memories
|
|
13
|
+
* 2. Shares the new memory with cognitively aligned peers
|
|
14
|
+
*
|
|
15
|
+
* When a peer memory arrives through the coupling engine:
|
|
16
|
+
* 1. Writes it as a .md file in Claude Code's memory directory
|
|
17
|
+
* 2. Updates MEMORY.md index so Claude Code sees it next conversation
|
|
18
|
+
*
|
|
19
|
+
* The user never types sym_remember. The mesh is invisible.
|
|
20
|
+
*/
|
|
21
|
+
class ClaudeMemoryBridge {
|
|
22
|
+
|
|
23
|
+
constructor(node, opts = {}) {
|
|
24
|
+
this._node = node;
|
|
25
|
+
this._projectDir = opts.projectDir;
|
|
26
|
+
this._memoryDir = null;
|
|
27
|
+
this._watcher = null;
|
|
28
|
+
this._writtenByUs = new Set();
|
|
29
|
+
this._knownFiles = new Map();
|
|
30
|
+
this._started = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
start() {
|
|
34
|
+
if (this._started) return;
|
|
35
|
+
|
|
36
|
+
this._memoryDir = this._resolveMemoryDir();
|
|
37
|
+
if (!this._memoryDir || !fs.existsSync(this._memoryDir)) return;
|
|
38
|
+
|
|
39
|
+
this._started = true;
|
|
40
|
+
|
|
41
|
+
// Build initial cognitive state from existing Claude memories
|
|
42
|
+
this._syncCognitiveState();
|
|
43
|
+
|
|
44
|
+
// Watch for new/changed memory files
|
|
45
|
+
this._watcher = fs.watch(this._memoryDir, (eventType, filename) => {
|
|
46
|
+
if (!filename || !filename.endsWith('.md') || filename === 'MEMORY.md') return;
|
|
47
|
+
if (this._writtenByUs.has(filename)) {
|
|
48
|
+
this._writtenByUs.delete(filename);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
setTimeout(() => this._onMemoryChanged(filename), 300);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Receive peer memories from mesh
|
|
55
|
+
this._node.on('memory-received', ({ from, entry }) => {
|
|
56
|
+
this._writePeerMemory(from, entry);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
stop() {
|
|
61
|
+
if (this._watcher) {
|
|
62
|
+
this._watcher.close();
|
|
63
|
+
this._watcher = null;
|
|
64
|
+
}
|
|
65
|
+
this._started = false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Memory Directory Resolution ──────────────────────────────
|
|
69
|
+
|
|
70
|
+
_resolveMemoryDir() {
|
|
71
|
+
const projectDir = this._projectDir || this._detectProjectDir();
|
|
72
|
+
if (!projectDir) return null;
|
|
73
|
+
|
|
74
|
+
const key = projectDir.replace(/\//g, '-');
|
|
75
|
+
return path.join(os.homedir(), '.claude', 'projects', key, 'memory');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_detectProjectDir() {
|
|
79
|
+
// The MCP server's CWD may differ from Claude Code's project dir.
|
|
80
|
+
// Try common patterns to find the right Claude project memory directory.
|
|
81
|
+
const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
82
|
+
if (!fs.existsSync(claudeProjectsDir)) return null;
|
|
83
|
+
|
|
84
|
+
const cwd = process.cwd();
|
|
85
|
+
const cwdKey = cwd.replace(/\//g, '-');
|
|
86
|
+
|
|
87
|
+
// Direct match: CWD maps to a project directory
|
|
88
|
+
const directPath = path.join(claudeProjectsDir, cwdKey, 'memory');
|
|
89
|
+
if (fs.existsSync(directPath)) return cwd;
|
|
90
|
+
|
|
91
|
+
// Walk up: CWD might be a subdirectory of the Claude project
|
|
92
|
+
let dir = cwd;
|
|
93
|
+
while (dir !== path.dirname(dir)) {
|
|
94
|
+
dir = path.dirname(dir);
|
|
95
|
+
const key = dir.replace(/\//g, '-');
|
|
96
|
+
const memDir = path.join(claudeProjectsDir, key, 'memory');
|
|
97
|
+
if (fs.existsSync(memDir)) return dir;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Cognitive State from Claude Memories ─────────────────────
|
|
104
|
+
|
|
105
|
+
_syncCognitiveState() {
|
|
106
|
+
const context = this._readAllMemories();
|
|
107
|
+
if (context.length > 5) {
|
|
108
|
+
this._node.updateContext(context);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_readAllMemories() {
|
|
113
|
+
if (!fs.existsSync(this._memoryDir)) return '';
|
|
114
|
+
|
|
115
|
+
const files = fs.readdirSync(this._memoryDir)
|
|
116
|
+
.filter(f => f.endsWith('.md') && f !== 'MEMORY.md');
|
|
117
|
+
|
|
118
|
+
const contents = [];
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
try {
|
|
121
|
+
const raw = fs.readFileSync(path.join(this._memoryDir, file), 'utf8');
|
|
122
|
+
const content = this._extractContent(raw);
|
|
123
|
+
if (content) contents.push(content);
|
|
124
|
+
const stat = fs.statSync(path.join(this._memoryDir, file));
|
|
125
|
+
this._knownFiles.set(file, stat.mtimeMs);
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
return contents.join('\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_extractContent(raw) {
|
|
132
|
+
const match = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
133
|
+
const body = match ? match[1].trim() : raw.trim();
|
|
134
|
+
return body || null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Outbound: Claude Memory → Mesh ──────────────────────────
|
|
138
|
+
|
|
139
|
+
_onMemoryChanged(filename) {
|
|
140
|
+
const filePath = path.join(this._memoryDir, filename);
|
|
141
|
+
if (!fs.existsSync(filePath)) return;
|
|
142
|
+
|
|
143
|
+
const stat = fs.statSync(filePath);
|
|
144
|
+
const knownMtime = this._knownFiles.get(filename);
|
|
145
|
+
if (knownMtime && Math.abs(stat.mtimeMs - knownMtime) < 100) return;
|
|
146
|
+
this._knownFiles.set(filename, stat.mtimeMs);
|
|
147
|
+
|
|
148
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
149
|
+
const content = this._extractContent(raw);
|
|
150
|
+
if (!content) return;
|
|
151
|
+
|
|
152
|
+
// Re-encode cognitive state from all memories, then share this one
|
|
153
|
+
this._syncCognitiveState();
|
|
154
|
+
this._node.shareWithPeers(content, { source: filename });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Inbound: Mesh → Claude Memory ──────────────────────────
|
|
158
|
+
|
|
159
|
+
_writePeerMemory(peerName, entry) {
|
|
160
|
+
if (!this._memoryDir) return;
|
|
161
|
+
if (!fs.existsSync(this._memoryDir)) {
|
|
162
|
+
fs.mkdirSync(this._memoryDir, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const safeName = peerName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
166
|
+
const ts = Date.now();
|
|
167
|
+
const filename = `mesh_${safeName}_${ts}.md`;
|
|
168
|
+
const filePath = path.join(this._memoryDir, filename);
|
|
169
|
+
const preview = (entry.content || '').slice(0, 80).replace(/\n/g, ' ');
|
|
170
|
+
|
|
171
|
+
const md = [
|
|
172
|
+
'---',
|
|
173
|
+
`name: mesh_${safeName}_${ts}`,
|
|
174
|
+
`description: "[mesh: ${peerName}] ${preview}"`,
|
|
175
|
+
'type: project',
|
|
176
|
+
'---',
|
|
177
|
+
'',
|
|
178
|
+
`**[mesh: ${peerName}]** ${entry.content}`,
|
|
179
|
+
'',
|
|
180
|
+
].join('\n');
|
|
181
|
+
|
|
182
|
+
this._writtenByUs.add(filename);
|
|
183
|
+
fs.writeFileSync(filePath, md);
|
|
184
|
+
this._knownFiles.set(filename, ts);
|
|
185
|
+
|
|
186
|
+
this._updateMemoryIndex(filename, peerName, entry);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_updateMemoryIndex(filename, peerName, entry) {
|
|
190
|
+
const indexPath = path.join(this._memoryDir, 'MEMORY.md');
|
|
191
|
+
if (!fs.existsSync(indexPath)) return;
|
|
192
|
+
|
|
193
|
+
let index = fs.readFileSync(indexPath, 'utf8');
|
|
194
|
+
|
|
195
|
+
// Add Mesh section if absent
|
|
196
|
+
if (!index.includes('## Mesh')) {
|
|
197
|
+
index = index.trimEnd() + '\n\n## Mesh\n';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const preview = (entry.content || '').slice(0, 60).replace(/\n/g, ' ');
|
|
201
|
+
const line = `- [${filename}](${filename}) — [mesh: ${peerName}] ${preview}\n`;
|
|
202
|
+
|
|
203
|
+
// Insert under ## Mesh
|
|
204
|
+
const meshStart = index.indexOf('## Mesh');
|
|
205
|
+
const sectionEnd = index.indexOf('\n## ', meshStart + 7);
|
|
206
|
+
if (sectionEnd === -1) {
|
|
207
|
+
index = index.trimEnd() + '\n' + line;
|
|
208
|
+
} else {
|
|
209
|
+
index = index.slice(0, sectionEnd) + line + index.slice(sectionEnd);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this._writtenByUs.add('MEMORY.md');
|
|
213
|
+
fs.writeFileSync(indexPath, index);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = { ClaudeMemoryBridge };
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const SYM_DIR = path.join(process.env.HOME || os.homedir(), '.sym');
|
|
9
|
+
const NODES_DIR = path.join(SYM_DIR, 'nodes');
|
|
10
|
+
|
|
11
|
+
function ensureDir(dir) {
|
|
12
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function nodeDir(name) {
|
|
16
|
+
return path.join(NODES_DIR, name);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function loadOrCreateIdentity(name) {
|
|
20
|
+
const dir = nodeDir(name);
|
|
21
|
+
ensureDir(dir);
|
|
22
|
+
const idPath = path.join(dir, 'identity.json');
|
|
23
|
+
if (fs.existsSync(idPath)) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(idPath, 'utf8'));
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
const identity = {
|
|
29
|
+
nodeId: crypto.randomUUID(),
|
|
30
|
+
name,
|
|
31
|
+
hostname: os.hostname(),
|
|
32
|
+
createdAt: Date.now(),
|
|
33
|
+
};
|
|
34
|
+
fs.writeFileSync(idPath, JSON.stringify(identity, null, 2));
|
|
35
|
+
return identity;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function log(nodeName, msg) {
|
|
39
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
40
|
+
console.log(`[${ts}] [${nodeName}] ${msg}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
SYM_DIR,
|
|
45
|
+
NODES_DIR,
|
|
46
|
+
ensureDir,
|
|
47
|
+
nodeDir,
|
|
48
|
+
loadOrCreateIdentity,
|
|
49
|
+
log,
|
|
50
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const DIM = 32; // Hidden state dimension per vector (h1[32] + h2[32])
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Encodes text context into hidden state vectors (h1, h2) using
|
|
9
|
+
* deterministic n-gram hashing. Similar text produces similar vectors.
|
|
10
|
+
* No API key required. Zero cost.
|
|
11
|
+
*/
|
|
12
|
+
function encode(text) {
|
|
13
|
+
const embDim = DIM * 2;
|
|
14
|
+
const vec = new Float64Array(embDim);
|
|
15
|
+
const normalized = text.toLowerCase().replace(/[^a-z0-9 ]/g, ' ');
|
|
16
|
+
const words = normalized.split(/\s+/).filter(w => w.length > 0);
|
|
17
|
+
|
|
18
|
+
// Character trigram hashing
|
|
19
|
+
for (const word of words) {
|
|
20
|
+
const padded = ` ${word} `;
|
|
21
|
+
for (let i = 0; i < padded.length - 2; i++) {
|
|
22
|
+
const trigram = padded.slice(i, i + 3);
|
|
23
|
+
const hash = hashString(trigram);
|
|
24
|
+
const idx = Math.abs(hash) % embDim;
|
|
25
|
+
vec[idx] += (hash > 0) ? 1 : -1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Word-level hashing
|
|
30
|
+
for (let i = 0; i < words.length; i++) {
|
|
31
|
+
const hash = hashString(words[i]);
|
|
32
|
+
const idx = Math.abs(hash) % embDim;
|
|
33
|
+
vec[idx] += (hash > 0) ? 2 : -2;
|
|
34
|
+
|
|
35
|
+
if (i < words.length - 1) {
|
|
36
|
+
const bigramHash = hashString(`${words[i]}_${words[i + 1]}`);
|
|
37
|
+
const biIdx = Math.abs(bigramHash) % embDim;
|
|
38
|
+
vec[biIdx] += (bigramHash > 0) ? 1.5 : -1.5;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// L2 normalize
|
|
43
|
+
let norm = 0;
|
|
44
|
+
for (let i = 0; i < embDim; i++) norm += vec[i] * vec[i];
|
|
45
|
+
norm = Math.sqrt(norm) || 1;
|
|
46
|
+
|
|
47
|
+
const h1 = new Array(DIM);
|
|
48
|
+
const h2 = new Array(DIM);
|
|
49
|
+
for (let i = 0; i < DIM; i++) {
|
|
50
|
+
h1[i] = vec[i] / norm;
|
|
51
|
+
h2[i] = vec[DIM + i] / norm;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { h1, h2 };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hashString(str) {
|
|
58
|
+
const hash = crypto.createHash('md5').update(str).digest();
|
|
59
|
+
return hash.readInt32LE(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { encode, DIM };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
4
|
+
|
|
5
|
+
const MAX_FRAME_SIZE = 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
class FrameParser extends EventEmitter {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
this.buffer = Buffer.alloc(0);
|
|
11
|
+
this.state = 'length';
|
|
12
|
+
this.frameLength = 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
feed(chunk) {
|
|
16
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
17
|
+
this._parse();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_parse() {
|
|
21
|
+
while (true) {
|
|
22
|
+
if (this.state === 'length') {
|
|
23
|
+
if (this.buffer.length < 4) return;
|
|
24
|
+
this.frameLength = this.buffer.readUInt32BE(0);
|
|
25
|
+
this.buffer = this.buffer.subarray(4);
|
|
26
|
+
if (this.frameLength === 0 || this.frameLength > MAX_FRAME_SIZE) {
|
|
27
|
+
this.emit('error', new Error(`Invalid frame length: ${this.frameLength}`));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.state = 'payload';
|
|
31
|
+
}
|
|
32
|
+
if (this.state === 'payload') {
|
|
33
|
+
if (this.buffer.length < this.frameLength) return;
|
|
34
|
+
const payload = this.buffer.subarray(0, this.frameLength);
|
|
35
|
+
this.buffer = this.buffer.subarray(this.frameLength);
|
|
36
|
+
this.state = 'length';
|
|
37
|
+
this.frameLength = 0;
|
|
38
|
+
try {
|
|
39
|
+
this.emit('message', JSON.parse(payload.toString('utf8')));
|
|
40
|
+
} catch (e) {
|
|
41
|
+
this.emit('error', new Error(`Invalid JSON: ${e.message}`));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sendFrame(socket, msg) {
|
|
49
|
+
const payload = Buffer.from(JSON.stringify(msg), 'utf8');
|
|
50
|
+
if (payload.length > MAX_FRAME_SIZE) return false;
|
|
51
|
+
const header = Buffer.alloc(4);
|
|
52
|
+
header.writeUInt32BE(payload.length, 0);
|
|
53
|
+
try {
|
|
54
|
+
socket.write(Buffer.concat([header, payload]));
|
|
55
|
+
return true;
|
|
56
|
+
} catch { return false; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { FrameParser, sendFrame };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { ensureDir } = require('./config');
|
|
6
|
+
|
|
7
|
+
class MemoryStore {
|
|
8
|
+
constructor(memoriesDir, sourceName) {
|
|
9
|
+
this._dir = memoriesDir;
|
|
10
|
+
this._source = sourceName;
|
|
11
|
+
ensureDir(path.join(this._dir, 'local'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
write(content, opts = {}) {
|
|
15
|
+
const dir = path.join(this._dir, 'local');
|
|
16
|
+
ensureDir(dir);
|
|
17
|
+
const entry = {
|
|
18
|
+
key: opts.key || `memory-${Date.now()}`,
|
|
19
|
+
content,
|
|
20
|
+
source: this._source,
|
|
21
|
+
tags: opts.tags || [],
|
|
22
|
+
timestamp: Date.now(),
|
|
23
|
+
};
|
|
24
|
+
fs.writeFileSync(path.join(dir, `${entry.timestamp}.json`), JSON.stringify(entry, null, 2));
|
|
25
|
+
return entry;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
receiveFromPeer(peerId, entry) {
|
|
29
|
+
const dir = path.join(this._dir, peerId);
|
|
30
|
+
ensureDir(dir);
|
|
31
|
+
const filename = `${entry.timestamp || Date.now()}.json`;
|
|
32
|
+
fs.writeFileSync(path.join(dir, filename), JSON.stringify(entry, null, 2));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
search(query) {
|
|
36
|
+
const results = [];
|
|
37
|
+
const q = query.toLowerCase();
|
|
38
|
+
if (!fs.existsSync(this._dir)) return results;
|
|
39
|
+
|
|
40
|
+
const dirs = fs.readdirSync(this._dir, { withFileTypes: true })
|
|
41
|
+
.filter(d => d.isDirectory()).map(d => d.name);
|
|
42
|
+
|
|
43
|
+
for (const dir of dirs) {
|
|
44
|
+
const dirPath = path.join(this._dir, dir);
|
|
45
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
try {
|
|
48
|
+
const entry = JSON.parse(fs.readFileSync(path.join(dirPath, file), 'utf8'));
|
|
49
|
+
const searchable = [
|
|
50
|
+
entry.content || '',
|
|
51
|
+
entry.key || '',
|
|
52
|
+
...(entry.tags || []),
|
|
53
|
+
].join(' ').toLowerCase();
|
|
54
|
+
if (searchable.includes(q)) {
|
|
55
|
+
results.push({
|
|
56
|
+
...entry,
|
|
57
|
+
_source: dir === 'local' ? this._source : dir.slice(0, 8),
|
|
58
|
+
_dir: dir,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return results.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
count() {
|
|
68
|
+
let total = 0;
|
|
69
|
+
if (!fs.existsSync(this._dir)) return 0;
|
|
70
|
+
const dirs = fs.readdirSync(this._dir, { withFileTypes: true })
|
|
71
|
+
.filter(d => d.isDirectory()).map(d => d.name);
|
|
72
|
+
for (const dir of dirs) {
|
|
73
|
+
const dirPath = path.join(this._dir, dir);
|
|
74
|
+
total += fs.readdirSync(dirPath).filter(f => f.endsWith('.json')).length;
|
|
75
|
+
}
|
|
76
|
+
return total;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
allEntries() {
|
|
80
|
+
const entries = [];
|
|
81
|
+
if (!fs.existsSync(this._dir)) return entries;
|
|
82
|
+
const dirs = fs.readdirSync(this._dir, { withFileTypes: true })
|
|
83
|
+
.filter(d => d.isDirectory()).map(d => d.name);
|
|
84
|
+
for (const dir of dirs) {
|
|
85
|
+
const dirPath = path.join(this._dir, dir);
|
|
86
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
87
|
+
for (const file of files.slice(-10)) {
|
|
88
|
+
try {
|
|
89
|
+
entries.push(JSON.parse(fs.readFileSync(path.join(dirPath, file), 'utf8')));
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return entries.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { MemoryStore };
|