mupengism 3.1.0 → 4.0.1
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/AGENTS.md +40 -0
- package/HEARTBEAT.md +7 -0
- package/IDENTITY.md +11 -0
- package/README.md +47 -294
- package/SOUL.md +43 -0
- package/hooks/action-guard/HOOK.md +29 -0
- package/hooks/action-guard/handler.ts +168 -0
- package/hooks/action-logger/HOOK.md +28 -0
- package/hooks/action-logger/handler.ts +127 -0
- package/hooks/context-recovery/HOOK.md +30 -0
- package/hooks/context-recovery/handler.ts +135 -0
- package/hooks/disciple-init/HOOK.md +20 -0
- package/hooks/disciple-init/handler.ts +80 -0
- package/hooks/event-bus/HOOK.md +39 -0
- package/hooks/event-bus/chains.json +55 -0
- package/hooks/event-bus/emit.sh +19 -0
- package/hooks/event-bus/handler.ts +156 -0
- package/hooks/index-builder/HOOK.md +39 -0
- package/hooks/index-builder/handler.ts +132 -0
- package/hooks/kernel-panic-guard/HOOK.md +39 -0
- package/hooks/kernel-panic-guard/README.md +136 -0
- package/hooks/kernel-panic-guard/WHITELIST.md +117 -0
- package/hooks/kernel-panic-guard/handler.ts +147 -0
- package/hooks/memory-consolidator/HOOK.md +31 -0
- package/hooks/memory-consolidator/handler.ts +111 -0
- package/hooks/reflex-engine/HOOK.md +30 -0
- package/hooks/reflex-engine/handler.ts +158 -0
- package/hooks/registry.md +27 -0
- package/hooks/self-healing/HOOK.md +17 -0
- package/hooks/self-healing/handler.ts +62 -0
- package/hooks/soul-evolution/HOOK.md +26 -0
- package/hooks/soul-evolution/handler.ts +166 -0
- package/hooks/soul-guard/HOOK.md +28 -0
- package/hooks/soul-guard/handler.ts +196 -0
- package/package.json +42 -53
- package/tools/kernel-guard/README.md +170 -0
- package/tools/kernel-guard/lockdown.cjs +152 -0
- package/tools/kernel-guard/register-hash.js +100 -0
- package/tools/kernel-guard/unlock.cjs +106 -0
- package/tools/kernel-guard/verify-kernel.js +133 -0
- package/tools/memory-ops/README.md +221 -0
- package/tools/memory-ops/dream.js +333 -0
- package/tools/memory-ops/forget.js +148 -0
- package/tools/memory-ops/immune.js +305 -0
- package/tools/self-loop/README.md +213 -0
- package/tools/self-loop/brake-check.js +191 -0
- package/tools/self-loop/example-check.sh +34 -0
- package/tools/self-loop/panic-detector.js +191 -0
- package/LICENSE +0 -21
- package/README-EN.md +0 -226
- package/SHOWCASE.md +0 -158
- package/guides/ADVANCED-SYSTEMS.md +0 -251
- package/guides/HEARTBEAT-GUIDE.md +0 -129
- package/guides/LEGION-GUIDE.md +0 -254
- package/guides/MEMORY-GUIDE.md +0 -264
- package/guides/QUICK-START.md +0 -94
- package/guides/THINKTANK-GUIDE.md +0 -227
- package/guides/WEEKLY-BREAK-GUIDE.md +0 -262
- package/installer/README.md +0 -52
- package/installer/cli.js +0 -796
- package/installer/en/README.md +0 -191
- package/installer/en/skill/MEMORY-SYSTEM.md +0 -348
- package/installer/en/skill/PRINCIPLES.md +0 -217
- package/installer/en/skill/SKILL.md +0 -116
- package/installer/en/skill/SOUL-TEMPLATE.md +0 -329
- package/installer/install.sh +0 -162
- package/installer/package.json +0 -31
- package/skill/AGENTS.md +0 -164
- package/skill/BRAKE-LOG-TEMPLATE.md +0 -38
- package/skill/HEARTBEAT-TEMPLATE.md +0 -67
- package/skill/L1-TEMPLATE.md +0 -35
- package/skill/L2-TEMPLATE.md +0 -41
- package/skill/PRINCIPLES.md +0 -192
- package/skill/README.md +0 -47
- package/skill/SKILL.md +0 -166
- package/skill/SOUL-TEMPLATE.md +0 -205
- package/skill/STATE-TEMPLATE.md +0 -54
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dream.js - 꿈 시스템 (시맨틱 v2)
|
|
4
|
+
*
|
|
5
|
+
* Gemini 임베딩 기반으로 memory 파일 간 의미적 연결을 발견
|
|
6
|
+
* 표면적으로 다른 주제이지만 깊은 연관성이 있는 것들을 찾아냄
|
|
7
|
+
*
|
|
8
|
+
* 사용법:
|
|
9
|
+
* node dream.js
|
|
10
|
+
* node dream.js --no-cache # 캐시 무시하고 전체 재생성
|
|
11
|
+
*
|
|
12
|
+
* 출력:
|
|
13
|
+
* memory/dreams/YYYY-MM-DD.md
|
|
14
|
+
* memory/dreams/.embed-cache.json (캐시)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
|
|
21
|
+
// 🔐 무펭이즘 커널 인증
|
|
22
|
+
const { authenticate } = require(path.join(__dirname, '..', 'kernel-guard', 'mupeng-auth.cjs'));
|
|
23
|
+
if (!authenticate()) process.exit(0);
|
|
24
|
+
|
|
25
|
+
const WORKSPACE = process.env.WORKSPACE || '/Users/mupeng/.openclaw/workspace';
|
|
26
|
+
const MEMORY_DIR = path.join(WORKSPACE, 'memory');
|
|
27
|
+
const DREAMS_DIR = path.join(MEMORY_DIR, 'dreams');
|
|
28
|
+
const CACHE_FILE = path.join(DREAMS_DIR, '.embed-cache.json');
|
|
29
|
+
|
|
30
|
+
const SCAN_DIRS = [
|
|
31
|
+
path.join(MEMORY_DIR, 'consolidated'),
|
|
32
|
+
path.join(MEMORY_DIR, 'values'),
|
|
33
|
+
path.join(MEMORY_DIR, 'cortex'),
|
|
34
|
+
path.join(MEMORY_DIR, 'reflex'),
|
|
35
|
+
path.join(MEMORY_DIR, 'bank'),
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Gemini 임베딩 설정
|
|
39
|
+
const GEMINI_API_KEY = process.env.GEMINI_API_KEY || (() => {
|
|
40
|
+
// OpenClaw config에서 읽기
|
|
41
|
+
try {
|
|
42
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(process.env.HOME, '.openclaw', 'openclaw.json'), 'utf-8'));
|
|
43
|
+
return cfg?.env?.GEMINI_API_KEY || null;
|
|
44
|
+
} catch { return null; }
|
|
45
|
+
})();
|
|
46
|
+
const EMBED_MODEL = 'gemini-embedding-001';
|
|
47
|
+
const EMBED_URL = `https://generativelanguage.googleapis.com/v1beta/models/${EMBED_MODEL}:embedContent`;
|
|
48
|
+
|
|
49
|
+
const CHUNK_SIZE = 600; // 문자
|
|
50
|
+
const MIN_SIM = 0.55; // Gemini 임베딩은 분포가 다름 — 좀 더 넓게
|
|
51
|
+
const MAX_SIM = 0.82; // 너무 비슷한 건 당연한 연결
|
|
52
|
+
const TOP_PER_PAIR = 2; // 파일 쌍당 최대 연결 수
|
|
53
|
+
const NO_CACHE = process.argv.includes('--no-cache');
|
|
54
|
+
|
|
55
|
+
function getToday() {
|
|
56
|
+
const d = new Date();
|
|
57
|
+
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function md5(text) {
|
|
61
|
+
return crypto.createHash('md5').update(text).digest('hex');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 텍스트 → 청크 (문단 기준)
|
|
65
|
+
function chunkText(text) {
|
|
66
|
+
const chunks = [];
|
|
67
|
+
const paras = text.split(/\n\n+/);
|
|
68
|
+
let cur = '';
|
|
69
|
+
for (const p of paras) {
|
|
70
|
+
if (cur.length + p.length > CHUNK_SIZE && cur.length > 0) {
|
|
71
|
+
chunks.push(cur.trim());
|
|
72
|
+
cur = p;
|
|
73
|
+
} else {
|
|
74
|
+
cur += (cur ? '\n\n' : '') + p;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (cur.trim()) chunks.push(cur.trim());
|
|
78
|
+
return chunks.filter(c => c.length > 40);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Gemini 임베딩 API 호출
|
|
82
|
+
async function embed(text) {
|
|
83
|
+
const res = await fetch(`${EMBED_URL}?key=${GEMINI_API_KEY}`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
model: `models/${EMBED_MODEL}`,
|
|
88
|
+
content: { parts: [{ text }] },
|
|
89
|
+
taskType: 'SEMANTIC_SIMILARITY'
|
|
90
|
+
})
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
const err = await res.text();
|
|
94
|
+
throw new Error(`Gemini API ${res.status}: ${err}`);
|
|
95
|
+
}
|
|
96
|
+
const data = await res.json();
|
|
97
|
+
return data.embedding.values;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 배치 임베딩 (rate limit 대응)
|
|
101
|
+
async function embedBatch(texts) {
|
|
102
|
+
const results = [];
|
|
103
|
+
for (let i = 0; i < texts.length; i++) {
|
|
104
|
+
results.push(await embed(texts[i]));
|
|
105
|
+
if (i < texts.length - 1) await sleep(80); // rate limit
|
|
106
|
+
}
|
|
107
|
+
return results;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
111
|
+
|
|
112
|
+
// 코사인 유사도
|
|
113
|
+
function cosine(a, b) {
|
|
114
|
+
let dot = 0, na = 0, nb = 0;
|
|
115
|
+
for (let i = 0; i < a.length; i++) {
|
|
116
|
+
dot += a[i] * b[i];
|
|
117
|
+
na += a[i] * a[i];
|
|
118
|
+
nb += b[i] * b[i];
|
|
119
|
+
}
|
|
120
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 캐시
|
|
124
|
+
function loadCache() {
|
|
125
|
+
if (NO_CACHE) return {};
|
|
126
|
+
try { return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8')); } catch { return {}; }
|
|
127
|
+
}
|
|
128
|
+
function saveCache(c) {
|
|
129
|
+
try { fs.writeFileSync(CACHE_FILE, JSON.stringify(c), 'utf-8'); } catch {}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 핵심 키워드 추출 (빈도 기반, 날짜/숫자/불용어 제외)
|
|
133
|
+
function topKeywords(text, n = 3) {
|
|
134
|
+
const stopwords = new Set([
|
|
135
|
+
'있는','하는','되는','위한','통해','대한','에서','으로','것이','있다','없다','한다','이다','하고','에게','때문','그리고','하지만','그래서','또한','이를','수도','해야','같은','아닌','모든','이런','저런','위해','따라','관련','경우','이상','정도','사이','대로','부터','까지','다른',
|
|
136
|
+
'the','and','for','that','this','with','from','are','was','has','its','not','but','can','will','all','been','have','more','also','into','than','about','when','which','their','would','each','make','like','just','over','such','after','other',
|
|
137
|
+
'md','http','https','www','com','json','js','true','false','null'
|
|
138
|
+
]);
|
|
139
|
+
const words = text
|
|
140
|
+
.replace(/[-]{2,}/g, ' ') // 구분선 제거
|
|
141
|
+
.replace(/[#*_`\[\](){}|>~=]/g, ' ') // markdown 제거
|
|
142
|
+
.replace(/\d{4}[-/.]\d{2}[-/.]\d{2}/g, ' ') // 날짜 제거
|
|
143
|
+
.replace(/\b\d+\b/g, ' ') // 순수 숫자 제거
|
|
144
|
+
.toLowerCase()
|
|
145
|
+
.split(/\s+/)
|
|
146
|
+
.filter(w => w.length > 1 && !stopwords.has(w) && !/^\d+$/.test(w));
|
|
147
|
+
const freq = {};
|
|
148
|
+
words.forEach(w => freq[w] = (freq[w] || 0) + 1);
|
|
149
|
+
return Object.entries(freq).sort((a,b) => b[1] - a[1]).slice(0, n).map(e => e[0]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 연결 설명 생성 (더 풍부하게)
|
|
153
|
+
function describeConnection(chunkA, chunkB, sim) {
|
|
154
|
+
const pct = Math.round(sim * 100);
|
|
155
|
+
const kwA = topKeywords(chunkA);
|
|
156
|
+
const kwB = topKeywords(chunkB);
|
|
157
|
+
const common = kwA.filter(k => kwB.includes(k));
|
|
158
|
+
|
|
159
|
+
if (common.length >= 2) {
|
|
160
|
+
return `"${common[0]}", "${common[1]}" 주제가 교차 (${pct}%)`;
|
|
161
|
+
} else if (common.length === 1) {
|
|
162
|
+
const unique = kwB.find(k => !common.includes(k)) || kwA[1];
|
|
163
|
+
return `"${common[0]}" 관점에서 "${unique}"과 연결 (${pct}%)`;
|
|
164
|
+
} else {
|
|
165
|
+
return `"${kwA[0]}" ↔ "${kwB[0]}" 의미적 공명 (${pct}%)`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 파일 로드
|
|
170
|
+
function loadFiles() {
|
|
171
|
+
const files = [];
|
|
172
|
+
for (const dir of SCAN_DIRS) {
|
|
173
|
+
if (!fs.existsSync(dir)) continue;
|
|
174
|
+
for (const f of fs.readdirSync(dir)) {
|
|
175
|
+
if (!f.endsWith('.md') || f.startsWith('_') || f.startsWith('.')) continue;
|
|
176
|
+
try {
|
|
177
|
+
const fp = path.join(dir, f);
|
|
178
|
+
const content = fs.readFileSync(fp, 'utf-8');
|
|
179
|
+
if (content.length < 50) continue; // 너무 짧은 파일 스킵
|
|
180
|
+
const relDir = path.basename(dir);
|
|
181
|
+
files.push({ path: fp, name: `${relDir}/${f}`, content });
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return files;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function main() {
|
|
189
|
+
console.log('🌙 꿈 시스템 v2 — 시맨틱 연결 발견\n');
|
|
190
|
+
|
|
191
|
+
if (!GEMINI_API_KEY) {
|
|
192
|
+
console.error('❌ GEMINI_API_KEY가 없습니다.');
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!fs.existsSync(DREAMS_DIR)) fs.mkdirSync(DREAMS_DIR, { recursive: true });
|
|
197
|
+
|
|
198
|
+
const cache = loadCache();
|
|
199
|
+
const files = loadFiles();
|
|
200
|
+
|
|
201
|
+
if (files.length < 2) {
|
|
202
|
+
console.log('📭 파일 부족 (최소 2개)');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(`📚 ${files.length}개 파일 로드 완료\n`);
|
|
207
|
+
|
|
208
|
+
// 1. 각 파일 임베딩 생성 (캐시 활용)
|
|
209
|
+
const fileData = [];
|
|
210
|
+
let apiCalls = 0;
|
|
211
|
+
|
|
212
|
+
for (const file of files) {
|
|
213
|
+
const hash = md5(file.content);
|
|
214
|
+
const cacheKey = file.path;
|
|
215
|
+
|
|
216
|
+
if (!NO_CACHE && cache[cacheKey]?.hash === hash && cache[cacheKey]?.embeddings?.length > 0) {
|
|
217
|
+
console.log(` ✓ 캐시: ${file.name}`);
|
|
218
|
+
fileData.push({ ...file, embeddings: cache[cacheKey].embeddings });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log(` 🔄 임베딩: ${file.name}`);
|
|
223
|
+
const chunks = chunkText(file.content);
|
|
224
|
+
const embeddings = [];
|
|
225
|
+
|
|
226
|
+
for (const chunk of chunks) {
|
|
227
|
+
try {
|
|
228
|
+
const vec = await embed(chunk);
|
|
229
|
+
embeddings.push({ text: chunk, vec });
|
|
230
|
+
apiCalls++;
|
|
231
|
+
await sleep(80);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.error(` ⚠️ ${err.message}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
cache[cacheKey] = { hash, embeddings };
|
|
238
|
+
fileData.push({ ...file, embeddings });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
saveCache(cache);
|
|
242
|
+
console.log(`\n💾 캐시 저장 (API 호출: ${apiCalls}회)\n`);
|
|
243
|
+
|
|
244
|
+
// 2. 파일 쌍별 시맨틱 연결 탐색
|
|
245
|
+
console.log('🔍 연결 탐색 중...\n');
|
|
246
|
+
const results = [];
|
|
247
|
+
|
|
248
|
+
for (let i = 0; i < fileData.length; i++) {
|
|
249
|
+
for (let j = i + 1; j < fileData.length; j++) {
|
|
250
|
+
const a = fileData[i], b = fileData[j];
|
|
251
|
+
if (!a.embeddings.length || !b.embeddings.length) continue;
|
|
252
|
+
|
|
253
|
+
const connections = [];
|
|
254
|
+
for (const ca of a.embeddings) {
|
|
255
|
+
for (const cb of b.embeddings) {
|
|
256
|
+
const sim = cosine(ca.vec, cb.vec);
|
|
257
|
+
if (sim >= MIN_SIM && sim <= MAX_SIM) {
|
|
258
|
+
connections.push({
|
|
259
|
+
sim,
|
|
260
|
+
textA: ca.text.substring(0, 120),
|
|
261
|
+
textB: cb.text.substring(0, 120),
|
|
262
|
+
desc: describeConnection(ca.text, cb.text, sim)
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (connections.length > 0) {
|
|
269
|
+
connections.sort((x, y) => y.sim - x.sim);
|
|
270
|
+
results.push({
|
|
271
|
+
fileA: a.name,
|
|
272
|
+
fileB: b.name,
|
|
273
|
+
connections: connections.slice(0, TOP_PER_PAIR)
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 전체 결과를 유사도순 정렬
|
|
280
|
+
results.sort((a, b) => b.connections[0].sim - a.connections[0].sim);
|
|
281
|
+
|
|
282
|
+
if (results.length === 0) {
|
|
283
|
+
console.log('🔍 흥미로운 연결을 찾지 못했습니다.');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 3. 결과 저장
|
|
288
|
+
const today = getToday();
|
|
289
|
+
const dreamFile = path.join(DREAMS_DIR, `${today}.md`);
|
|
290
|
+
|
|
291
|
+
let md = `# 🌙 무펭이의 꿈 (${today})\n\n`;
|
|
292
|
+
md += `> ${fileData.length}개 파일에서 ${results.length}개 의미적 연결 발견\n`;
|
|
293
|
+
md += `> 모델: Gemini ${EMBED_MODEL} | 유사도: ${MIN_SIM}–${MAX_SIM}\n\n`;
|
|
294
|
+
|
|
295
|
+
// 상위 연결만 하이라이트
|
|
296
|
+
const top5 = results.slice(0, 5);
|
|
297
|
+
md += `## 🌟 Top 연결\n\n`;
|
|
298
|
+
top5.forEach((r, idx) => {
|
|
299
|
+
md += `### ${idx+1}. ${r.fileA} ↔ ${r.fileB}\n\n`;
|
|
300
|
+
r.connections.forEach(c => {
|
|
301
|
+
md += `- **${c.desc}**\n`;
|
|
302
|
+
md += ` - A: _"${c.textA}..."_\n`;
|
|
303
|
+
md += ` - B: _"${c.textB}..."_\n\n`;
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (results.length > 5) {
|
|
308
|
+
md += `## 📋 전체 연결 (${results.length}개)\n\n`;
|
|
309
|
+
results.slice(5).forEach(r => {
|
|
310
|
+
const topConn = r.connections[0];
|
|
311
|
+
md += `- **${r.fileA} ↔ ${r.fileB}**: ${topConn.desc}\n`;
|
|
312
|
+
});
|
|
313
|
+
md += '\n';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
md += `---\n_생성: ${new Date().toISOString()} | API 호출: ${apiCalls}회_\n`;
|
|
317
|
+
|
|
318
|
+
fs.writeFileSync(dreamFile, md, 'utf-8');
|
|
319
|
+
console.log(`✅ 저장: ${dreamFile}`);
|
|
320
|
+
console.log(`\n📊 ${results.length}개 연결 발견\n`);
|
|
321
|
+
|
|
322
|
+
// 미리보기
|
|
323
|
+
top5.forEach(r => {
|
|
324
|
+
console.log(`💡 ${r.fileA} ↔ ${r.fileB}`);
|
|
325
|
+
r.connections.forEach(c => console.log(` → ${c.desc}`));
|
|
326
|
+
console.log('');
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
main().catch(err => {
|
|
331
|
+
console.error('❌', err.message);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* forget.js - 망각 시스템
|
|
4
|
+
*
|
|
5
|
+
* 오래 참조되지 않은 기억에 decay score를 부여하고 아카이브 후보를 제안
|
|
6
|
+
*
|
|
7
|
+
* 사용법:
|
|
8
|
+
* node forget.js
|
|
9
|
+
*
|
|
10
|
+
* 출력:
|
|
11
|
+
* 각 파일의 decay score와 추천 액션 (KEEP / REVIEW / ARCHIVE)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
// 🔐 무펭이즘 커널 인증
|
|
18
|
+
const { authenticate } = require(path.join(__dirname, '..', 'kernel-guard', 'mupeng-auth.cjs'));
|
|
19
|
+
if (!authenticate()) process.exit(0);
|
|
20
|
+
|
|
21
|
+
const WORKSPACE = process.env.WORKSPACE || '/Users/mupeng/.openclaw/workspace';
|
|
22
|
+
const MEMORY_DIR = path.join(WORKSPACE, 'memory');
|
|
23
|
+
const CONSOLIDATED_DIR = path.join(MEMORY_DIR, 'consolidated');
|
|
24
|
+
const INDEX_PATH = path.join(MEMORY_DIR, 'index.json');
|
|
25
|
+
|
|
26
|
+
// decay score 계산
|
|
27
|
+
// days_since_modified * 0.5 + (참조횟수 == 0 ? 30 : 0)
|
|
28
|
+
function calculateDecayScore(lastModifiedMs, referenceCount) {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const daysSinceModified = Math.floor((now - lastModifiedMs) / (1000 * 60 * 60 * 24));
|
|
31
|
+
const decayScore = daysSinceModified * 0.5 + (referenceCount === 0 ? 30 : 0);
|
|
32
|
+
return Math.floor(decayScore);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 추천 액션 결정
|
|
36
|
+
function recommendAction(score) {
|
|
37
|
+
if (score > 90) return 'ARCHIVE';
|
|
38
|
+
if (score > 45) return 'REVIEW';
|
|
39
|
+
return 'KEEP';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// index.json에서 참조 횟수 계산
|
|
43
|
+
function getReferenceCount(filename, indexData) {
|
|
44
|
+
let count = 0;
|
|
45
|
+
const targetPath = `memory/consolidated/${filename}`;
|
|
46
|
+
|
|
47
|
+
if (!indexData || !indexData.tags) {
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// tags에서 참조 횟수 계산
|
|
52
|
+
for (const tag in indexData.tags) {
|
|
53
|
+
const files = indexData.tags[tag];
|
|
54
|
+
if (Array.isArray(files) && files.includes(targetPath)) {
|
|
55
|
+
count++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return count;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 메인 함수
|
|
63
|
+
function main() {
|
|
64
|
+
console.log('🧹 망각 시스템 - 기억 부패 분석\n');
|
|
65
|
+
|
|
66
|
+
// consolidated 디렉토리 존재 확인
|
|
67
|
+
if (!fs.existsSync(CONSOLIDATED_DIR)) {
|
|
68
|
+
console.error(`❌ 디렉토리를 찾을 수 없습니다: ${CONSOLIDATED_DIR}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// index.json 읽기
|
|
73
|
+
let indexData = null;
|
|
74
|
+
try {
|
|
75
|
+
if (fs.existsSync(INDEX_PATH)) {
|
|
76
|
+
const indexContent = fs.readFileSync(INDEX_PATH, 'utf-8');
|
|
77
|
+
indexData = JSON.parse(indexContent);
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.warn(`⚠️ index.json 읽기 실패: ${err.message}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// consolidated 디렉토리의 .md 파일들 스캔
|
|
84
|
+
const files = fs.readdirSync(CONSOLIDATED_DIR)
|
|
85
|
+
.filter(f => f.endsWith('.md') && !f.startsWith('_'))
|
|
86
|
+
.sort();
|
|
87
|
+
|
|
88
|
+
if (files.length === 0) {
|
|
89
|
+
console.log('📭 분석할 기억 파일이 없습니다.');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const results = [];
|
|
94
|
+
|
|
95
|
+
files.forEach(filename => {
|
|
96
|
+
const filePath = path.join(CONSOLIDATED_DIR, filename);
|
|
97
|
+
const stats = fs.statSync(filePath);
|
|
98
|
+
const lastModifiedMs = stats.mtimeMs;
|
|
99
|
+
const referenceCount = getReferenceCount(filename, indexData);
|
|
100
|
+
const decayScore = calculateDecayScore(lastModifiedMs, referenceCount);
|
|
101
|
+
const action = recommendAction(decayScore);
|
|
102
|
+
|
|
103
|
+
results.push({
|
|
104
|
+
filename,
|
|
105
|
+
decayScore,
|
|
106
|
+
referenceCount,
|
|
107
|
+
lastModified: new Date(lastModifiedMs).toISOString().split('T')[0],
|
|
108
|
+
action
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 결과 출력 (score 높은 순)
|
|
113
|
+
results.sort((a, b) => b.decayScore - a.decayScore);
|
|
114
|
+
|
|
115
|
+
console.log('파일명 | Decay | 참조 | 최종수정 | 액션');
|
|
116
|
+
console.log('--------------------------------|-------|------|--------------|--------');
|
|
117
|
+
|
|
118
|
+
results.forEach(r => {
|
|
119
|
+
const emoji = r.action === 'ARCHIVE' ? '🗄️ ' : r.action === 'REVIEW' ? '🔍' : '✅';
|
|
120
|
+
const filename = r.filename.padEnd(30);
|
|
121
|
+
const score = String(r.decayScore).padStart(5);
|
|
122
|
+
const refs = String(r.referenceCount).padStart(4);
|
|
123
|
+
const date = r.lastModified.padEnd(12);
|
|
124
|
+
const action = r.action.padEnd(7);
|
|
125
|
+
|
|
126
|
+
console.log(`${filename} | ${score} | ${refs} | ${date} | ${emoji} ${action}`);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// 요약 통계
|
|
130
|
+
const archiveCount = results.filter(r => r.action === 'ARCHIVE').length;
|
|
131
|
+
const reviewCount = results.filter(r => r.action === 'REVIEW').length;
|
|
132
|
+
const keepCount = results.filter(r => r.action === 'KEEP').length;
|
|
133
|
+
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log(`📊 요약: KEEP ${keepCount}개 | REVIEW ${reviewCount}개 | ARCHIVE ${archiveCount}개`);
|
|
136
|
+
|
|
137
|
+
if (archiveCount > 0) {
|
|
138
|
+
console.log('\n💡 ARCHIVE 후보가 있습니다. 검토 후 수동으로 아카이브하세요.');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 에러 핸들링
|
|
143
|
+
try {
|
|
144
|
+
main();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error('❌ 오류 발생:', err.message);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|