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.
Files changed (77) hide show
  1. package/AGENTS.md +40 -0
  2. package/HEARTBEAT.md +7 -0
  3. package/IDENTITY.md +11 -0
  4. package/README.md +47 -294
  5. package/SOUL.md +43 -0
  6. package/hooks/action-guard/HOOK.md +29 -0
  7. package/hooks/action-guard/handler.ts +168 -0
  8. package/hooks/action-logger/HOOK.md +28 -0
  9. package/hooks/action-logger/handler.ts +127 -0
  10. package/hooks/context-recovery/HOOK.md +30 -0
  11. package/hooks/context-recovery/handler.ts +135 -0
  12. package/hooks/disciple-init/HOOK.md +20 -0
  13. package/hooks/disciple-init/handler.ts +80 -0
  14. package/hooks/event-bus/HOOK.md +39 -0
  15. package/hooks/event-bus/chains.json +55 -0
  16. package/hooks/event-bus/emit.sh +19 -0
  17. package/hooks/event-bus/handler.ts +156 -0
  18. package/hooks/index-builder/HOOK.md +39 -0
  19. package/hooks/index-builder/handler.ts +132 -0
  20. package/hooks/kernel-panic-guard/HOOK.md +39 -0
  21. package/hooks/kernel-panic-guard/README.md +136 -0
  22. package/hooks/kernel-panic-guard/WHITELIST.md +117 -0
  23. package/hooks/kernel-panic-guard/handler.ts +147 -0
  24. package/hooks/memory-consolidator/HOOK.md +31 -0
  25. package/hooks/memory-consolidator/handler.ts +111 -0
  26. package/hooks/reflex-engine/HOOK.md +30 -0
  27. package/hooks/reflex-engine/handler.ts +158 -0
  28. package/hooks/registry.md +27 -0
  29. package/hooks/self-healing/HOOK.md +17 -0
  30. package/hooks/self-healing/handler.ts +62 -0
  31. package/hooks/soul-evolution/HOOK.md +26 -0
  32. package/hooks/soul-evolution/handler.ts +166 -0
  33. package/hooks/soul-guard/HOOK.md +28 -0
  34. package/hooks/soul-guard/handler.ts +196 -0
  35. package/package.json +42 -53
  36. package/tools/kernel-guard/README.md +170 -0
  37. package/tools/kernel-guard/lockdown.cjs +152 -0
  38. package/tools/kernel-guard/register-hash.js +100 -0
  39. package/tools/kernel-guard/unlock.cjs +106 -0
  40. package/tools/kernel-guard/verify-kernel.js +133 -0
  41. package/tools/memory-ops/README.md +221 -0
  42. package/tools/memory-ops/dream.js +333 -0
  43. package/tools/memory-ops/forget.js +148 -0
  44. package/tools/memory-ops/immune.js +305 -0
  45. package/tools/self-loop/README.md +213 -0
  46. package/tools/self-loop/brake-check.js +191 -0
  47. package/tools/self-loop/example-check.sh +34 -0
  48. package/tools/self-loop/panic-detector.js +191 -0
  49. package/LICENSE +0 -21
  50. package/README-EN.md +0 -226
  51. package/SHOWCASE.md +0 -158
  52. package/guides/ADVANCED-SYSTEMS.md +0 -251
  53. package/guides/HEARTBEAT-GUIDE.md +0 -129
  54. package/guides/LEGION-GUIDE.md +0 -254
  55. package/guides/MEMORY-GUIDE.md +0 -264
  56. package/guides/QUICK-START.md +0 -94
  57. package/guides/THINKTANK-GUIDE.md +0 -227
  58. package/guides/WEEKLY-BREAK-GUIDE.md +0 -262
  59. package/installer/README.md +0 -52
  60. package/installer/cli.js +0 -796
  61. package/installer/en/README.md +0 -191
  62. package/installer/en/skill/MEMORY-SYSTEM.md +0 -348
  63. package/installer/en/skill/PRINCIPLES.md +0 -217
  64. package/installer/en/skill/SKILL.md +0 -116
  65. package/installer/en/skill/SOUL-TEMPLATE.md +0 -329
  66. package/installer/install.sh +0 -162
  67. package/installer/package.json +0 -31
  68. package/skill/AGENTS.md +0 -164
  69. package/skill/BRAKE-LOG-TEMPLATE.md +0 -38
  70. package/skill/HEARTBEAT-TEMPLATE.md +0 -67
  71. package/skill/L1-TEMPLATE.md +0 -35
  72. package/skill/L2-TEMPLATE.md +0 -41
  73. package/skill/PRINCIPLES.md +0 -192
  74. package/skill/README.md +0 -47
  75. package/skill/SKILL.md +0 -166
  76. package/skill/SOUL-TEMPLATE.md +0 -205
  77. 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
+ }