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.
- package/dist/agents/index.js +1 -1
- package/dist/agents/manager.js +30 -0
- package/dist/ai/claude.js +4 -3
- package/dist/cron/parser.js +153 -20
- package/dist/cron/scheduler.js +7 -3
- package/dist/heartbeat/index.js +5 -1
- package/dist/telegram/bot.js +3 -3
- package/dist/telegram/handlers/commands.js +39 -6
- package/dist/telegram/handlers/messages.js +115 -5
- package/dist/telegram/utils/index.js +2 -0
- package/dist/telegram/utils/prompt.js +10 -7
- package/dist/telegram/utils/secrets.js +64 -0
- package/dist/tools/index.js +360 -19
- package/dist/workspace/index.js +3 -1
- package/dist/workspace/load.js +251 -7
- package/package.json +1 -1
- package/templates/AGENTS.md +77 -10
- package/templates/MEMORY.md +58 -4
|
@@ -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
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -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,
|
|
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:
|
|
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: ["
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
1037
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/workspace/index.js
CHANGED
|
@@ -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,
|
|
6
|
+
export { loadWorkspace, loadBootstrap, saveWorkspaceFile, appendToMemory, loadRecentMemories, loadTodayAndYesterdayMemories,
|
|
7
|
+
// 메모리 정리 도구용
|
|
8
|
+
listDailyMemoryFiles, getDailyMemoryContent, appendToLongTermMemory, deleteDailyMemory, deleteOldDailyMemories, loadLongTermMemory, } from "./load.js";
|