companionbot 0.4.3 → 0.5.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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * API 키 및 시크릿 감지 유틸리티
3
+ * 사용자가 실수로 채팅에 API 키를 입력하는 것을 방지합니다.
4
+ */
5
+ // API 키 패턴들 (정규식)
6
+ const API_KEY_PATTERNS = [
7
+ // OpenAI
8
+ { name: "OpenAI", pattern: /sk-[a-zA-Z0-9]{20,}/ },
9
+ { name: "OpenAI Project", pattern: /sk-proj-[a-zA-Z0-9_-]{20,}/ },
10
+ // Anthropic
11
+ { name: "Anthropic", pattern: /sk-ant-[a-zA-Z0-9_-]{20,}/ },
12
+ { name: "Anthropic API", pattern: /anthropic-[a-zA-Z0-9_-]{20,}/ },
13
+ // Bearer tokens (일반)
14
+ { name: "Bearer Token", pattern: /Bearer\s+[a-zA-Z0-9_\-.]{20,}/ },
15
+ // GitHub
16
+ { name: "GitHub", pattern: /gh[pousr]_[a-zA-Z0-9]{20,}/ },
17
+ // Slack
18
+ { name: "Slack", pattern: /xox[bpras]-[a-zA-Z0-9\-]{20,}/ },
19
+ // Google
20
+ { name: "Google API", pattern: /AIza[a-zA-Z0-9_-]{35}/ },
21
+ // AWS
22
+ { name: "AWS Access Key", pattern: /AKIA[A-Z0-9]{16}/ },
23
+ { name: "AWS Secret", pattern: /[a-zA-Z0-9/+=]{40}(?=\s|$)/ },
24
+ // Telegram Bot Token
25
+ { name: "Telegram Bot", pattern: /\d{8,10}:[a-zA-Z0-9_-]{35}/ },
26
+ // Discord
27
+ { name: "Discord", pattern: /[MN][a-zA-Z0-9]{23,}\.[a-zA-Z0-9_-]{6}\.[a-zA-Z0-9_-]{27,}/ },
28
+ // Stripe
29
+ { name: "Stripe", pattern: /sk_live_[a-zA-Z0-9]{20,}/ },
30
+ { name: "Stripe Test", pattern: /sk_test_[a-zA-Z0-9]{20,}/ },
31
+ // Twilio
32
+ { name: "Twilio", pattern: /SK[a-f0-9]{32}/ },
33
+ // SendGrid
34
+ { name: "SendGrid", pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/ },
35
+ // Private keys
36
+ { name: "Private Key", pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/ },
37
+ // Generic API key patterns (높은 엔트로피 문자열)
38
+ { name: "API Key", pattern: /(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?token)\s*[:=]\s*['"]?[a-zA-Z0-9_\-]{20,}['"]?/i },
39
+ ];
40
+ /**
41
+ * 메시지에서 API 키나 시크릿을 감지합니다.
42
+ * @param message 검사할 메시지
43
+ * @returns 감지 결과
44
+ */
45
+ export function detectSecrets(message) {
46
+ const detectedTypes = [];
47
+ for (const { name, pattern } of API_KEY_PATTERNS) {
48
+ if (pattern.test(message)) {
49
+ detectedTypes.push(name);
50
+ }
51
+ }
52
+ return {
53
+ detected: detectedTypes.length > 0,
54
+ types: [...new Set(detectedTypes)], // 중복 제거
55
+ };
56
+ }
57
+ /**
58
+ * 메시지에 API 키가 포함되어 있는지 빠르게 확인합니다.
59
+ * @param message 검사할 메시지
60
+ * @returns API 키 포함 여부
61
+ */
62
+ export function containsSecret(message) {
63
+ return API_KEY_PATTERNS.some(({ pattern }) => pattern.test(message));
64
+ }
@@ -7,7 +7,9 @@ import { randomUUID } from "crypto";
7
7
  import { MODELS } from "../ai/claude.js";
8
8
  import { getCurrentChatId, setModel, getModel } from "../session/state.js";
9
9
  // Note: getCurrentChatId uses AsyncLocalStorage - must be called within runWithChatId context
10
- import { getWorkspacePath, saveWorkspaceFile, appendToMemory, deleteBootstrap, } from "../workspace/index.js";
10
+ import { getWorkspacePath, saveWorkspaceFile, appendToMemory, deleteBootstrap,
11
+ // 메모리 정리 도구용
12
+ listDailyMemoryFiles, getDailyMemoryContent, appendToLongTermMemory, deleteOldDailyMemories, loadLongTermMemory, } from "../workspace/index.js";
11
13
  import { getSecret } from "../config/secrets.js";
12
14
  import { createReminder, deleteReminder, getReminders, parseTimeExpression, } from "../reminders/index.js";
13
15
  import { isCalendarConfigured, getEvents, addEvent, deleteEvent, formatEvent, parseDateExpression, } from "../calendar/index.js";
@@ -16,6 +18,7 @@ import { setBriefingConfig, getBriefingConfig, disableBriefing, sendBriefingNow,
16
18
  import { spawnAgent, listAgents, cancelAgent, } from "../agents/index.js";
17
19
  import { addCronJob, listCronJobs, removeCronJob, setCronJobEnabled, runCronJobNow, parseScheduleExpression, } from "../cron/index.js";
18
20
  import * as cheerio from "cheerio";
21
+ import * as dns from "dns/promises";
19
22
  const execAsync = promisify(exec);
20
23
  // 메모리에 세션 저장
21
24
  const sessions = new Map();
@@ -349,18 +352,34 @@ Guidelines:
349
352
  },
350
353
  {
351
354
  name: "save_memory",
352
- description: "Save important information about the user or conversation to long-term memory. Use this when you learn something new about the user that should be remembered.",
355
+ description: `Save important information to daily memory. Use when you learn something about the user.
356
+
357
+ Features:
358
+ - Duplicate check: Won't save if similar content exists today (80% similarity threshold)
359
+ - Categories help organize memories for later retrieval
360
+
361
+ Guidelines:
362
+ - Use "preference" for likes/dislikes (음식, 음악, 스타일 등)
363
+ - Use "project" for work/projects (코드, 진행상황 등)
364
+ - Use "event" for dates/events (생일, 약속, 기념일)
365
+ - Use "decision" for decisions/agreements (결정, 약속)
366
+ - Use "user_info" for personal info (이름, 직업 등)
367
+ - Use force_save=true to bypass duplicate check if needed`,
353
368
  input_schema: {
354
369
  type: "object",
355
370
  properties: {
356
371
  content: {
357
372
  type: "string",
358
- description: "The information to remember",
373
+ description: "The information to remember. Be concise but specific.",
359
374
  },
360
375
  category: {
361
376
  type: "string",
362
- enum: ["user_info", "preference", "event", "project", "other"],
363
- description: "Category of the memory",
377
+ enum: ["preference", "project", "event", "decision", "user_info", "other"],
378
+ description: "Category of the memory (affects emoji and organization)",
379
+ },
380
+ force_save: {
381
+ type: "boolean",
382
+ description: "Skip duplicate check and save anyway (default: false)",
364
383
  },
365
384
  },
366
385
  required: ["content"],
@@ -782,6 +801,103 @@ Examples:
782
801
  required: ["id"],
783
802
  },
784
803
  },
804
+ // ============== 메모리 정리 도구 ==============
805
+ {
806
+ name: "list_daily_memories",
807
+ description: `List all daily memory files with their dates and sizes. Use to see what memories exist before organizing or cleaning.
808
+
809
+ Use this when:
810
+ - User asks what memories exist
811
+ - Before organizing memories to see available files
812
+ - Before cleaning old memories to check what will be affected`,
813
+ input_schema: {
814
+ type: "object",
815
+ properties: {
816
+ max_age_days: {
817
+ type: "number",
818
+ description: "Only show files older than this many days (optional)",
819
+ },
820
+ },
821
+ required: [],
822
+ },
823
+ },
824
+ {
825
+ name: "get_daily_memory",
826
+ description: "Get the content of a specific daily memory file by date.",
827
+ input_schema: {
828
+ type: "object",
829
+ properties: {
830
+ date: {
831
+ type: "string",
832
+ description: "Date in YYYY-MM-DD format (e.g., '2025-02-07')",
833
+ },
834
+ },
835
+ required: ["date"],
836
+ },
837
+ },
838
+ {
839
+ name: "organize_memory",
840
+ description: `Move important content from daily memory files to long-term MEMORY.md.
841
+
842
+ Use during heartbeats or when user asks to organize memories:
843
+ 1. Review recent daily files (call list_daily_memories first)
844
+ 2. Identify significant events, decisions, lessons, or insights
845
+ 3. Use this tool to save them to MEMORY.md with a section name
846
+ 4. Optionally clear old daily files with clear_old_memory
847
+
848
+ Example flow:
849
+ 1. list_daily_memories → see available files
850
+ 2. get_daily_memory date="2025-02-05" → review content
851
+ 3. organize_memory content="사용자는 오후에 집중력이 높다고 함" section="선호도"`,
852
+ input_schema: {
853
+ type: "object",
854
+ properties: {
855
+ content: {
856
+ type: "string",
857
+ description: "The important content to save to long-term memory",
858
+ },
859
+ section: {
860
+ type: "string",
861
+ description: "Optional section/category name (e.g., '선호도', '프로젝트', '결정사항')",
862
+ },
863
+ },
864
+ required: ["content"],
865
+ },
866
+ },
867
+ {
868
+ name: "clear_old_memory",
869
+ description: `Delete old daily memory files. Use after organizing to clean up storage.
870
+
871
+ Safety: Will not delete today's or yesterday's files by default.
872
+ Recommendation: Set older_than_days to at least 7 to keep a week of context.
873
+
874
+ Examples:
875
+ - "30일 지난 메모리 정리해줘" → older_than_days: 30
876
+ - "2주 이상 된 것만 삭제해" → older_than_days: 14`,
877
+ input_schema: {
878
+ type: "object",
879
+ properties: {
880
+ older_than_days: {
881
+ type: "number",
882
+ description: "Delete files older than this many days (minimum: 2, default: 30)",
883
+ },
884
+ dry_run: {
885
+ type: "boolean",
886
+ description: "If true, only show what would be deleted without actually deleting (default: true)",
887
+ },
888
+ },
889
+ required: [],
890
+ },
891
+ },
892
+ {
893
+ name: "get_long_term_memory",
894
+ description: "Read the current contents of MEMORY.md (long-term memory).",
895
+ input_schema: {
896
+ type: "object",
897
+ properties: {},
898
+ required: [],
899
+ },
900
+ },
785
901
  ];
786
902
  // Tool 실행 함수
787
903
  export async function executeTool(name, input) {
@@ -852,12 +968,50 @@ export async function executeTool(name, input) {
852
968
  "which", "env", "printenv"
853
969
  ];
854
970
  // 명령어 체이닝/치환/리디렉션 차단 (;, &&, ||, |, `, $(), ${}, 개행, >, <)
971
+ // Shell injection 방지: shell: false 사용하더라도 이중 방어
855
972
  if (/[;&|`\n\r]|\$\(|\$\{|>>|>|</.test(command)) {
856
973
  return `Error: Command chaining, substitution, and redirection not allowed.`;
857
974
  }
858
- // 번째 명령어 추출
859
- const parts = command.trim().split(/\s+/);
975
+ // 인자 파싱 (쉘을 거치지 않으므로 직접 파싱)
976
+ // 간단한 공백 분리 (따옴표 처리 포함)
977
+ const parseArgs = (cmd) => {
978
+ const args = [];
979
+ let current = "";
980
+ let inQuote = null;
981
+ for (let i = 0; i < cmd.length; i++) {
982
+ const char = cmd[i];
983
+ if (inQuote) {
984
+ if (char === inQuote) {
985
+ inQuote = null;
986
+ }
987
+ else {
988
+ current += char;
989
+ }
990
+ }
991
+ else if (char === '"' || char === "'") {
992
+ inQuote = char;
993
+ }
994
+ else if (char === " " || char === "\t") {
995
+ if (current) {
996
+ args.push(current);
997
+ current = "";
998
+ }
999
+ }
1000
+ else {
1001
+ current += char;
1002
+ }
1003
+ }
1004
+ if (current) {
1005
+ args.push(current);
1006
+ }
1007
+ return args;
1008
+ };
1009
+ const parts = parseArgs(command.trim());
1010
+ if (parts.length === 0) {
1011
+ return `Error: Empty command.`;
1012
+ }
860
1013
  const cmd = parts[0];
1014
+ const args = parts.slice(1);
861
1015
  if (!ALLOWED_COMMANDS.includes(cmd)) {
862
1016
  return `Error: Command '${cmd}' not in allowed list. Allowed: ${ALLOWED_COMMANDS.join(", ")}`;
863
1017
  }
@@ -866,6 +1020,13 @@ export async function executeTool(name, input) {
866
1020
  if (dangerousArgs.some(arg => parts.includes(arg))) {
867
1021
  return `Error: Dangerous argument detected.`;
868
1022
  }
1023
+ // 인자에 쉘 메타문자가 있으면 차단 (이중 방어)
1024
+ const shellMetaChars = /[;&|`$\\<>(){}[\]!#*?~]/;
1025
+ for (const arg of args) {
1026
+ if (shellMetaChars.test(arg)) {
1027
+ return `Error: Invalid characters in argument: ${arg}`;
1028
+ }
1029
+ }
869
1030
  // 환경 변수는 필요한 것만 화이트리스트로 전달 (민감 정보 노출 방지)
870
1031
  const safeEnv = {
871
1032
  PATH: process.env.PATH || "",
@@ -874,14 +1035,16 @@ export async function executeTool(name, input) {
874
1035
  LANG: process.env.LANG || "en_US.UTF-8",
875
1036
  TERM: process.env.TERM || "xterm",
876
1037
  };
877
- // Background 실행
1038
+ // Background 실행 - shell: false (spawn 직접 사용)
878
1039
  if (background) {
879
1040
  const sessionId = randomUUID().slice(0, 8);
880
- const child = spawn("sh", ["-c", command], {
1041
+ // shell: false - 쉘을 거치지 않고 직접 실행
1042
+ const child = spawn(cmd, args, {
881
1043
  cwd,
882
1044
  env: safeEnv,
883
1045
  detached: true,
884
1046
  stdio: ["ignore", "pipe", "pipe"],
1047
+ shell: false, // 명시적으로 shell 비활성화
885
1048
  });
886
1049
  const session = {
887
1050
  id: sessionId,
@@ -921,14 +1084,41 @@ CWD: ${cwd}
921
1084
 
922
1085
  Use list_sessions to see all sessions, get_session_log to view output, kill_session to terminate.`;
923
1086
  }
924
- // Foreground 실행 (기존 방식)
1087
+ // Foreground 실행 - spawn with shell: false
925
1088
  try {
926
- const { stdout, stderr } = await execAsync(command, {
927
- cwd,
928
- timeout,
929
- env: safeEnv,
1089
+ const result = await new Promise((resolve, reject) => {
1090
+ const child = spawn(cmd, args, {
1091
+ cwd,
1092
+ env: safeEnv,
1093
+ shell: false, // shell 비활성화
1094
+ });
1095
+ let stdout = "";
1096
+ let stderr = "";
1097
+ child.stdout?.on("data", (data) => {
1098
+ stdout += data.toString();
1099
+ });
1100
+ child.stderr?.on("data", (data) => {
1101
+ stderr += data.toString();
1102
+ });
1103
+ const timeoutId = setTimeout(() => {
1104
+ child.kill("SIGTERM");
1105
+ reject(new Error(`Command timed out after ${timeout / 1000}s`));
1106
+ }, timeout);
1107
+ child.on("close", (code) => {
1108
+ clearTimeout(timeoutId);
1109
+ if (code === 0) {
1110
+ resolve({ stdout, stderr });
1111
+ }
1112
+ else {
1113
+ reject(new Error(`Command failed with exit code ${code}: ${stderr || stdout}`));
1114
+ }
1115
+ });
1116
+ child.on("error", (err) => {
1117
+ clearTimeout(timeoutId);
1118
+ reject(err);
1119
+ });
930
1120
  });
931
- return stdout || stderr || "Command executed (no output)";
1121
+ return result.stdout || result.stderr || "Command executed (no output)";
932
1122
  }
933
1123
  catch (error) {
934
1124
  return `Error: ${error instanceof Error ? error.message : String(error)}`;
@@ -1033,8 +1223,15 @@ ${"─".repeat(40)}`;
1033
1223
  case "save_memory": {
1034
1224
  const content = input.content;
1035
1225
  const category = input.category || "other";
1036
- await appendToMemory(`[${category}] ${content}`);
1037
- return `Memory saved: ${content.slice(0, 50)}...`;
1226
+ const forceSave = input.force_save || false;
1227
+ const result = await appendToMemory(content, {
1228
+ category: category,
1229
+ skipDuplicateCheck: forceSave,
1230
+ });
1231
+ if (!result.success && result.isDuplicate) {
1232
+ return `Memory not saved (duplicate detected). Similar entry: "${result.existingEntry}..."`;
1233
+ }
1234
+ return `Memory saved [${category}]: ${content.slice(0, 50)}${content.length > 50 ? "..." : ""}`;
1038
1235
  }
1039
1236
  case "save_persona": {
1040
1237
  const identity = input.identity;
@@ -1375,9 +1572,11 @@ ${"─".repeat(40)}`;
1375
1572
  if (!url.startsWith("http://") && !url.startsWith("https://")) {
1376
1573
  return "Error: URL must start with http:// or https://";
1377
1574
  }
1378
- // SSRF 방지: 사설 IP 차단
1575
+ // SSRF 방지: 사설 IP 차단 + DNS rebinding 방지
1576
+ let parsedUrl;
1379
1577
  try {
1380
- const parsedUrl = new URL(url);
1578
+ parsedUrl = new URL(url);
1579
+ // 1차 검증: hostname이 직접 IP인 경우
1381
1580
  if (isPrivateIP(parsedUrl.hostname)) {
1382
1581
  return "Error: Access to private/internal addresses is not allowed.";
1383
1582
  }
@@ -1385,12 +1584,52 @@ ${"─".repeat(40)}`;
1385
1584
  catch {
1386
1585
  return "Error: Invalid URL format.";
1387
1586
  }
1587
+ // DNS rebinding 방지: 실제 IP 주소를 먼저 resolve하고 검증
1388
1588
  try {
1589
+ // IP 주소가 아닌 hostname만 DNS lookup 수행
1590
+ const isIPAddress = /^[\d.]+$/.test(parsedUrl.hostname) || parsedUrl.hostname.includes(":");
1591
+ if (!isIPAddress) {
1592
+ const addresses = await dns.lookup(parsedUrl.hostname, { all: true });
1593
+ for (const addr of addresses) {
1594
+ if (isPrivateIP(addr.address)) {
1595
+ return `Error: DNS resolves to private IP (${addr.address}). Access denied.`;
1596
+ }
1597
+ }
1598
+ }
1599
+ }
1600
+ catch (dnsError) {
1601
+ // DNS lookup 실패 - 도메인이 존재하지 않거나 resolve 불가
1602
+ return `Error: Could not resolve hostname: ${parsedUrl.hostname}`;
1603
+ }
1604
+ try {
1605
+ // fetch 시 redirect 제한 (redirect를 통한 SSRF 방지)
1606
+ const controller = new AbortController();
1607
+ const timeoutId = setTimeout(() => controller.abort(), 30000);
1389
1608
  const response = await fetch(url, {
1390
1609
  headers: {
1391
1610
  "User-Agent": "Mozilla/5.0 (compatible; CompanionBot/1.0)",
1392
1611
  },
1612
+ redirect: "manual", // redirect 직접 처리
1613
+ signal: controller.signal,
1393
1614
  });
1615
+ clearTimeout(timeoutId);
1616
+ // Redirect 처리: 새 URL도 검증
1617
+ if (response.status >= 300 && response.status < 400) {
1618
+ const redirectUrl = response.headers.get("location");
1619
+ if (redirectUrl) {
1620
+ try {
1621
+ const redirectParsed = new URL(redirectUrl, url);
1622
+ if (isPrivateIP(redirectParsed.hostname)) {
1623
+ return "Error: Redirect to private/internal address blocked.";
1624
+ }
1625
+ // 재귀 호출 대신 단일 redirect만 허용하고 에러 반환
1626
+ return `Error: URL redirects to ${redirectUrl}. Please use the final URL directly.`;
1627
+ }
1628
+ catch {
1629
+ return "Error: Invalid redirect URL.";
1630
+ }
1631
+ }
1632
+ }
1394
1633
  if (!response.ok) {
1395
1634
  return `Error: Failed to fetch URL (${response.status}: ${response.statusText})`;
1396
1635
  }
@@ -1554,6 +1793,96 @@ Next run: ${nextRunStr}`;
1554
1793
  return `Error: Cron job ${id} not found.`;
1555
1794
  }
1556
1795
  }
1796
+ // ============== 메모리 정리 도구 ==============
1797
+ case "list_daily_memories": {
1798
+ const maxAgeDays = input.max_age_days;
1799
+ const files = await listDailyMemoryFiles();
1800
+ if (files.length === 0) {
1801
+ return "No daily memory files found.";
1802
+ }
1803
+ // 필터링 (maxAgeDays가 지정된 경우)
1804
+ const filtered = maxAgeDays !== undefined
1805
+ ? files.filter(f => f.ageInDays >= maxAgeDays)
1806
+ : files;
1807
+ if (filtered.length === 0) {
1808
+ return `No memory files older than ${maxAgeDays} days.`;
1809
+ }
1810
+ const lines = filtered.map(f => {
1811
+ const sizeKb = (f.sizeBytes / 1024).toFixed(1);
1812
+ const ageLabel = f.ageInDays === 0 ? "오늘"
1813
+ : f.ageInDays === 1 ? "어제"
1814
+ : `${f.ageInDays}일 전`;
1815
+ return `📅 ${f.date} (${ageLabel}) - ${sizeKb}KB`;
1816
+ });
1817
+ const totalSize = filtered.reduce((sum, f) => sum + f.sizeBytes, 0);
1818
+ const totalKb = (totalSize / 1024).toFixed(1);
1819
+ return `Daily memory files (${filtered.length} files, ${totalKb}KB total):\n\n${lines.join("\n")}`;
1820
+ }
1821
+ case "get_daily_memory": {
1822
+ const date = input.date;
1823
+ if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
1824
+ return "Error: Date must be in YYYY-MM-DD format (e.g., '2025-02-07')";
1825
+ }
1826
+ const content = await getDailyMemoryContent(date);
1827
+ if (!content) {
1828
+ return `No memory file found for ${date}.`;
1829
+ }
1830
+ return `📅 ${date} 메모리:\n\n${content}`;
1831
+ }
1832
+ case "organize_memory": {
1833
+ const content = input.content;
1834
+ const section = input.section;
1835
+ if (!content || content.trim().length === 0) {
1836
+ return "Error: Content is required for organizing memory";
1837
+ }
1838
+ try {
1839
+ await appendToLongTermMemory(content.trim(), section);
1840
+ const sectionNote = section ? ` (섹션: ${section})` : "";
1841
+ return `✅ Long-term memory updated${sectionNote}.\n\nAdded:\n${content.slice(0, 200)}${content.length > 200 ? "..." : ""}`;
1842
+ }
1843
+ catch (error) {
1844
+ return `Error saving to long-term memory: ${error instanceof Error ? error.message : String(error)}`;
1845
+ }
1846
+ }
1847
+ case "clear_old_memory": {
1848
+ const olderThanDays = Math.max(2, input.older_than_days || 30);
1849
+ const dryRun = input.dry_run !== false; // default to true for safety
1850
+ const files = await listDailyMemoryFiles();
1851
+ const toDelete = files.filter(f => f.ageInDays >= olderThanDays);
1852
+ if (toDelete.length === 0) {
1853
+ return `No memory files older than ${olderThanDays} days to delete.`;
1854
+ }
1855
+ const totalSize = toDelete.reduce((sum, f) => sum + f.sizeBytes, 0);
1856
+ const totalKb = (totalSize / 1024).toFixed(1);
1857
+ if (dryRun) {
1858
+ const fileList = toDelete.slice(0, 10).map(f => ` - ${f.date}`).join("\n");
1859
+ const moreNote = toDelete.length > 10 ? `\n ... and ${toDelete.length - 10} more` : "";
1860
+ return `🔍 DRY RUN - Would delete ${toDelete.length} files (${totalKb}KB):\n${fileList}${moreNote}\n\nSet dry_run=false to actually delete.`;
1861
+ }
1862
+ // 실제 삭제
1863
+ const result = await deleteOldDailyMemories(olderThanDays);
1864
+ if (result.deleted.length === 0) {
1865
+ return "No files were deleted.";
1866
+ }
1867
+ const deletedList = result.deleted.slice(0, 5).join(", ");
1868
+ const moreNote = result.deleted.length > 5 ? ` and ${result.deleted.length - 5} more` : "";
1869
+ let response = `🗑️ Deleted ${result.deleted.length} memory files: ${deletedList}${moreNote}`;
1870
+ if (result.failed.length > 0) {
1871
+ response += `\n⚠️ Failed to delete: ${result.failed.join(", ")}`;
1872
+ }
1873
+ return response;
1874
+ }
1875
+ case "get_long_term_memory": {
1876
+ const content = await loadLongTermMemory();
1877
+ if (!content) {
1878
+ return "MEMORY.md does not exist yet. Use organize_memory to start building long-term memory.";
1879
+ }
1880
+ // 크기 제한 (너무 크면 요약 권장)
1881
+ if (content.length > 10000) {
1882
+ return `📚 MEMORY.md (${(content.length / 1024).toFixed(1)}KB - showing first 10KB):\n\n${content.slice(0, 10000)}\n\n... (truncated, full file is ${content.length} chars)`;
1883
+ }
1884
+ return `📚 MEMORY.md:\n\n${content}`;
1885
+ }
1557
1886
  default:
1558
1887
  return `Error: Unknown tool: ${name}`;
1559
1888
  }
@@ -1632,5 +1961,17 @@ export function getToolsDescription(modelId) {
1632
1961
  - toggle_cron: cron job 활성화/비활성화 (id, enabled)
1633
1962
  - run_cron: cron job 즉시 실행 (id) - 테스트/수동 트리거용
1634
1963
 
1964
+ ## 메모리 정리 (Long-term Memory Management)
1965
+ - list_daily_memories: 일일 메모리 파일 목록 조회
1966
+ - max_age_days: N일 이상된 것만 필터
1967
+ - get_daily_memory: 특정 날짜의 메모리 읽기 (date: "YYYY-MM-DD")
1968
+ - organize_memory: 중요한 내용을 MEMORY.md로 이동
1969
+ - content: 저장할 내용
1970
+ - section: 섹션명 (선택)
1971
+ - clear_old_memory: 오래된 일일 메모리 정리
1972
+ - older_than_days: N일 이상 (기본 30일, 최소 2일)
1973
+ - dry_run: true면 미리보기만 (기본 true)
1974
+ - get_long_term_memory: MEMORY.md 내용 읽기
1975
+
1635
1976
  허용된 경로: ${path.join(home, "Documents")}, ${path.join(home, "projects")}, 워크스페이스`;
1636
1977
  }
@@ -3,4 +3,6 @@ export { getWorkspacePath, getWorkspaceFilePath, getMemoryDirPath, getDailyMemor
3
3
  // 초기화 함수
4
4
  export { isWorkspaceInitialized, initWorkspace, hasBootstrap, deleteBootstrap, } from "./init.js";
5
5
  // 로드/저장 함수
6
- export { loadWorkspace, loadBootstrap, saveWorkspaceFile, appendToMemory, loadRecentMemories, } from "./load.js";
6
+ export { loadWorkspace, loadBootstrap, saveWorkspaceFile, appendToMemory, loadRecentMemories, loadTodayAndYesterdayMemories,
7
+ // 메모리 정리 도구용
8
+ listDailyMemoryFiles, getDailyMemoryContent, appendToLongTermMemory, deleteDailyMemory, deleteOldDailyMemories, loadLongTermMemory, } from "./load.js";