aiexecode 1.0.94 → 1.0.127

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 (80) hide show
  1. package/README.md +198 -88
  2. package/index.js +310 -86
  3. package/mcp-agent-lib/src/mcp_message_logger.js +17 -16
  4. package/package.json +4 -4
  5. package/payload_viewer/out/404/index.html +1 -1
  6. package/payload_viewer/out/404.html +1 -1
  7. package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
  8. package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
  9. package/payload_viewer/out/index.html +1 -1
  10. package/payload_viewer/out/index.txt +3 -3
  11. package/payload_viewer/web_server.js +361 -0
  12. package/prompts/completion_judge.txt +4 -0
  13. package/prompts/orchestrator.txt +116 -3
  14. package/src/LLMClient/client.js +401 -18
  15. package/src/LLMClient/converters/responses-to-claude.js +67 -18
  16. package/src/LLMClient/converters/responses-to-zai.js +667 -0
  17. package/src/LLMClient/errors.js +30 -4
  18. package/src/LLMClient/index.js +5 -0
  19. package/src/ai_based/completion_judge.js +263 -186
  20. package/src/ai_based/orchestrator.js +171 -35
  21. package/src/commands/agents.js +70 -0
  22. package/src/commands/apikey.js +1 -1
  23. package/src/commands/bg.js +129 -0
  24. package/src/commands/commands.js +51 -0
  25. package/src/commands/debug.js +52 -0
  26. package/src/commands/help.js +11 -1
  27. package/src/commands/model.js +42 -7
  28. package/src/commands/reasoning_effort.js +2 -2
  29. package/src/commands/skills.js +46 -0
  30. package/src/config/ai_models.js +106 -6
  31. package/src/config/constants.js +71 -0
  32. package/src/config/feature_flags.js +6 -7
  33. package/src/frontend/App.js +108 -1
  34. package/src/frontend/components/AutocompleteMenu.js +7 -1
  35. package/src/frontend/components/BackgroundProcessList.js +175 -0
  36. package/src/frontend/components/ConversationItem.js +26 -10
  37. package/src/frontend/components/CurrentModelView.js +2 -2
  38. package/src/frontend/components/HelpView.js +106 -2
  39. package/src/frontend/components/Input.js +33 -11
  40. package/src/frontend/components/ModelListView.js +1 -1
  41. package/src/frontend/components/SetupWizard.js +51 -8
  42. package/src/frontend/hooks/useFileCompletion.js +467 -0
  43. package/src/frontend/utils/toolUIFormatter.js +261 -0
  44. package/src/system/agents_loader.js +289 -0
  45. package/src/system/ai_request.js +156 -12
  46. package/src/system/background_process.js +317 -0
  47. package/src/system/code_executer.js +496 -56
  48. package/src/system/command_parser.js +33 -3
  49. package/src/system/conversation_state.js +265 -0
  50. package/src/system/conversation_trimmer.js +132 -0
  51. package/src/system/custom_command_loader.js +386 -0
  52. package/src/system/file_integrity.js +73 -10
  53. package/src/system/log.js +10 -2
  54. package/src/system/output_helper.js +52 -9
  55. package/src/system/session.js +213 -58
  56. package/src/system/session_memory.js +30 -2
  57. package/src/system/skill_loader.js +318 -0
  58. package/src/system/system_info.js +254 -40
  59. package/src/system/tool_approval.js +10 -0
  60. package/src/system/tool_registry.js +15 -1
  61. package/src/system/ui_events.js +11 -0
  62. package/src/tools/code_editor.js +16 -10
  63. package/src/tools/file_reader.js +66 -9
  64. package/src/tools/glob.js +0 -3
  65. package/src/tools/ripgrep.js +5 -7
  66. package/src/tools/skill_tool.js +122 -0
  67. package/src/tools/web_downloader.js +0 -3
  68. package/src/util/clone.js +174 -0
  69. package/src/util/config.js +55 -2
  70. package/src/util/config_migration.js +174 -0
  71. package/src/util/debug_log.js +8 -2
  72. package/src/util/exit_handler.js +8 -0
  73. package/src/util/file_reference_parser.js +132 -0
  74. package/src/util/path_validator.js +178 -0
  75. package/src/util/prompt_loader.js +91 -1
  76. package/src/util/safe_fs.js +66 -3
  77. package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
  78. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_buildManifest.js +0 -0
  79. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_clientMiddlewareManifest.json +0 -0
  80. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_ssgManifest.js +0 -0
@@ -1,6 +1,7 @@
1
1
  // 설정 파일 및 환경 관리 유틸리티
2
2
  import { homedir } from 'os';
3
3
  import { join, dirname } from 'path';
4
+ import { readFileSync } from 'fs';
4
5
  import { safeReadFile, safeWriteFile, safeMkdir, safeReaddir, safeStat, safeCopyFile } from './safe_fs.js';
5
6
  import { fileURLToPath } from 'url';
6
7
  import { DEFAULT_MODEL } from '../config/ai_models.js';
@@ -41,10 +42,20 @@ export const PAYLOAD_LOG_DIR = join(CONFIG_DIR, 'payload_log');
41
42
  export const PAYLOAD_LLM_LOG_DIR = join(CONFIG_DIR, 'payload_LLM_log');
42
43
  export const DEBUG_LOG_DIR = join(CONFIG_DIR, 'debuglog');
43
44
  export const DEBUG_LOG_FILE = join(CONFIG_DIR, 'debug.txt'); // Deprecated: 호환성을 위해 유지
45
+ // Skill 시스템 경로
46
+ export const PERSONAL_SKILLS_DIR = join(CONFIG_DIR, 'skills'); // ~/.aiexe/skills/
47
+ export const PROJECT_SKILLS_DIR = '.aiexe/skills'; // CWD/.aiexe/skills/ (상대경로)
48
+
49
+ // Custom Commands 시스템 경로 (Claude Code 스타일)
50
+ export const PERSONAL_COMMANDS_DIR = join(CONFIG_DIR, 'commands'); // ~/.aiexe/commands/
51
+ export const PROJECT_COMMANDS_DIR = '.aiexe/commands'; // CWD/.aiexe/commands/ (상대경로)
44
52
  const DEFAULT_SETTINGS = {
45
53
  API_KEY: '',
46
54
  MODEL: DEFAULT_MODEL,
55
+ BASE_URL: '', // 커스텀 API 엔드포인트 (비어있으면 기본값 사용)
47
56
  REASONING_EFFORT: 'medium', // 'minimal', 'low', 'medium', 'high'
57
+ // API 요청/응답 내용 화면 표시 여부
58
+ SHOW_API_PAYLOAD: false, // true: 요청/응답 내용을 화면에 표시, false: 표시하지 않음
48
59
  // 도구 활성화 옵션
49
60
  TOOLS_ENABLED: {
50
61
  edit_file_range: false, // 기본적으로 비활성화 (edit_file_replace 사용 권장)
@@ -61,6 +72,22 @@ const DEFAULT_SETTINGS = {
61
72
  }
62
73
  };
63
74
 
75
+ /**
76
+ * 디버그 모드(SHOW_API_PAYLOAD) 활성화 여부 확인 (동기)
77
+ * 로깅 함수에서 사용하기 위한 동기 버전
78
+ * @returns {boolean} 디버그 모드 활성화 여부
79
+ */
80
+ export function isDebugModeEnabled() {
81
+ try {
82
+ const data = readFileSync(SETTINGS_FILE, 'utf8');
83
+ const parsed = JSON.parse(data);
84
+ return parsed?.SHOW_API_PAYLOAD === true;
85
+ } catch {
86
+ // 설정 파일이 없거나 읽기 실패 시 기본값 false
87
+ return false;
88
+ }
89
+ }
90
+
64
91
  /**
65
92
  * 설정 디렉토리를 생성하고 템플릿 파일을 복사
66
93
  * @returns {Promise<void>}
@@ -74,7 +101,7 @@ export async function ensureConfigDirectory() {
74
101
  if (!templateDir) return;
75
102
 
76
103
  const templateFiles = await safeReaddir(templateDir);
77
- await Promise.all(
104
+ const copyResults = await Promise.allSettled(
78
105
  templateFiles.map(async (fileName) => {
79
106
  const sourcePath = join(templateDir, fileName);
80
107
  const stat = await safeStat(sourcePath);
@@ -82,8 +109,15 @@ export async function ensureConfigDirectory() {
82
109
  const destPath = join(CONFIG_DIR, fileName);
83
110
  await safeCopyFile(sourcePath, destPath);
84
111
  }
112
+ return fileName;
85
113
  })
86
114
  );
115
+ // 실패한 복사 작업은 조용히 무시 (설정 디렉토리 초기화 실패가 앱 전체를 막으면 안됨)
116
+ const failedCopies = copyResults.filter(r => r.status === 'rejected');
117
+ if (failedCopies.length > 0) {
118
+ // 로깅만 하고 에러는 던지지 않음
119
+ console.error(`[config] ${failedCopies.length} template file(s) failed to copy`);
120
+ }
87
121
  } catch (error) {
88
122
  // Failed to ensure config directory - silently ignore
89
123
  throw error;
@@ -131,7 +165,7 @@ export async function saveSettings(settings) {
131
165
  /**
132
166
  * API 키의 접두어를 기반으로 발급처를 판단합니다.
133
167
  * @param {string} apiKey - API 키 문자열
134
- * @returns {string|null} 발급처 이름 ("Google", "OpenAI", "Anthropic") 또는 null (알 수 없는 경우)
168
+ * @returns {string|null} 발급처 이름 ("Google", "OpenAI", "Anthropic", "Z.AI") 또는 null (알 수 없는 경우)
135
169
  */
136
170
  export function APIKeyIssuedFrom(apiKey) {
137
171
  if (!apiKey || typeof apiKey !== 'string') {
@@ -144,7 +178,26 @@ export function APIKeyIssuedFrom(apiKey) {
144
178
  return 'OpenAI';
145
179
  } else if (apiKey.startsWith('sk-ant-')) {
146
180
  return 'Anthropic';
181
+ } else if (/^[a-f0-9]{32}\.[A-Za-z0-9]{16}$/.test(apiKey)) {
182
+ // Z.AI API 키 형식: 32자리 hex + '.' + 16자리 영숫자
183
+ return 'Z.AI';
147
184
  }
148
185
 
149
186
  return null;
150
187
  }
188
+
189
+ /**
190
+ * Provider별 기본 BASE_URL 반환
191
+ * @param {string} provider - 프로바이더 이름 ('openai', 'claude', 'zai', 'gemini')
192
+ * @returns {string|null} 기본 BASE_URL 또는 null
193
+ */
194
+ export function getDefaultBaseUrlForProvider(provider) {
195
+ const baseUrls = {
196
+ 'zai': 'https://api.z.ai/api/anthropic',
197
+ // 다른 프로바이더는 SDK 기본값 사용
198
+ 'openai': null,
199
+ 'claude': null,
200
+ 'gemini': null
201
+ };
202
+ return baseUrls[provider] || null;
203
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Configuration Migration Utility
3
+ * 설정 버전 관리 및 자동 마이그레이션을 제공합니다.
4
+ */
5
+
6
+ import { safeReadFile, safeWriteFile, safeExists } from './safe_fs.js';
7
+ import { CONFIG_DIR, SETTINGS_FILE } from './config.js';
8
+ import { deepMerge } from './clone.js';
9
+
10
+ /** 현재 설정 버전 */
11
+ export const CURRENT_CONFIG_VERSION = 2;
12
+
13
+ /**
14
+ * 설정 마이그레이션 정의
15
+ * 각 버전에서 다음 버전으로의 마이그레이션 함수를 정의합니다.
16
+ */
17
+ const migrations = {
18
+ /**
19
+ * 버전 1 → 2 마이그레이션
20
+ * - TOOLS_ENABLED 구조 추가
21
+ * - REASONING_EFFORT 기본값 추가
22
+ */
23
+ 1: (settings) => {
24
+ const migrated = { ...settings };
25
+
26
+ // TOOLS_ENABLED가 없으면 추가
27
+ if (!migrated.TOOLS_ENABLED) {
28
+ migrated.TOOLS_ENABLED = {
29
+ edit_file_range: false,
30
+ edit_file_replace: true,
31
+ write_file: true,
32
+ read_file: true,
33
+ read_file_range: true,
34
+ bash: true,
35
+ run_python_code: false,
36
+ fetch_web_page: true,
37
+ response_message: true,
38
+ ripgrep: true,
39
+ glob_search: true
40
+ };
41
+ }
42
+
43
+ // REASONING_EFFORT가 없으면 추가
44
+ if (!migrated.REASONING_EFFORT) {
45
+ migrated.REASONING_EFFORT = 'medium';
46
+ }
47
+
48
+ migrated._config_version = 2;
49
+ return migrated;
50
+ }
51
+ };
52
+
53
+ /**
54
+ * 설정 버전을 확인합니다.
55
+ * @param {Object} settings - 설정 객체
56
+ * @returns {number} 설정 버전 (없으면 1 반환)
57
+ */
58
+ export function getConfigVersion(settings) {
59
+ return settings?._config_version || 1;
60
+ }
61
+
62
+ /**
63
+ * 설정이 마이그레이션이 필요한지 확인합니다.
64
+ * @param {Object} settings - 설정 객체
65
+ * @returns {boolean} 마이그레이션이 필요하면 true
66
+ */
67
+ export function needsMigration(settings) {
68
+ const version = getConfigVersion(settings);
69
+ return version < CURRENT_CONFIG_VERSION;
70
+ }
71
+
72
+ /**
73
+ * 설정을 최신 버전으로 마이그레이션합니다.
74
+ * @param {Object} settings - 설정 객체
75
+ * @returns {{settings: Object, migrated: boolean, fromVersion: number, toVersion: number}}
76
+ */
77
+ export function migrateSettings(settings) {
78
+ const fromVersion = getConfigVersion(settings);
79
+ let currentSettings = { ...settings };
80
+ let currentVersion = fromVersion;
81
+
82
+ // 순차적으로 마이그레이션 적용
83
+ while (currentVersion < CURRENT_CONFIG_VERSION) {
84
+ const migrationFn = migrations[currentVersion];
85
+ if (migrationFn) {
86
+ currentSettings = migrationFn(currentSettings);
87
+ currentVersion++;
88
+ } else {
89
+ // 마이그레이션 함수가 없으면 버전만 업데이트
90
+ currentSettings._config_version = currentVersion + 1;
91
+ currentVersion++;
92
+ }
93
+ }
94
+
95
+ return {
96
+ settings: currentSettings,
97
+ migrated: fromVersion !== currentVersion,
98
+ fromVersion,
99
+ toVersion: currentVersion
100
+ };
101
+ }
102
+
103
+ /**
104
+ * 설정 파일을 마이그레이션하고 저장합니다.
105
+ * @param {string} settingsPath - 설정 파일 경로 (선택, 기본값: SETTINGS_FILE)
106
+ * @returns {Promise<{migrated: boolean, fromVersion: number, toVersion: number}>}
107
+ */
108
+ export async function migrateSettingsFile(settingsPath = SETTINGS_FILE) {
109
+ try {
110
+ // 설정 파일 존재 확인
111
+ if (!(await safeExists(settingsPath))) {
112
+ return { migrated: false, fromVersion: CURRENT_CONFIG_VERSION, toVersion: CURRENT_CONFIG_VERSION };
113
+ }
114
+
115
+ // 설정 파일 읽기
116
+ const content = await safeReadFile(settingsPath, 'utf8');
117
+ const settings = JSON.parse(content);
118
+
119
+ // 마이그레이션 필요 여부 확인
120
+ if (!needsMigration(settings)) {
121
+ return { migrated: false, fromVersion: getConfigVersion(settings), toVersion: CURRENT_CONFIG_VERSION };
122
+ }
123
+
124
+ // 마이그레이션 수행
125
+ const result = migrateSettings(settings);
126
+
127
+ // 마이그레이션된 설정 저장
128
+ await safeWriteFile(settingsPath, JSON.stringify(result.settings, null, 2), 'utf8');
129
+
130
+ return {
131
+ migrated: result.migrated,
132
+ fromVersion: result.fromVersion,
133
+ toVersion: result.toVersion
134
+ };
135
+ } catch (error) {
136
+ console.error(`[config_migration] Migration failed: ${error.message}`);
137
+ return { migrated: false, fromVersion: 0, toVersion: 0, error: error.message };
138
+ }
139
+ }
140
+
141
+ /**
142
+ * 새 설정 객체를 기본값과 병합합니다.
143
+ * @param {Object} settings - 사용자 설정
144
+ * @param {Object} defaults - 기본 설정
145
+ * @returns {Object} 병합된 설정
146
+ */
147
+ export function mergeWithDefaults(settings, defaults) {
148
+ const merged = deepMerge(defaults, settings);
149
+ merged._config_version = CURRENT_CONFIG_VERSION;
150
+ return merged;
151
+ }
152
+
153
+ /**
154
+ * 설정 백업을 생성합니다.
155
+ * @param {string} settingsPath - 설정 파일 경로
156
+ * @returns {Promise<string>} 백업 파일 경로
157
+ */
158
+ export async function createSettingsBackup(settingsPath = SETTINGS_FILE) {
159
+ try {
160
+ if (!(await safeExists(settingsPath))) {
161
+ return null;
162
+ }
163
+
164
+ const content = await safeReadFile(settingsPath, 'utf8');
165
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
166
+ const backupPath = settingsPath.replace('.json', `.backup-${timestamp}.json`);
167
+
168
+ await safeWriteFile(backupPath, content, 'utf8');
169
+ return backupPath;
170
+ } catch (error) {
171
+ console.error(`[config_migration] Backup failed: ${error.message}`);
172
+ return null;
173
+ }
174
+ }
@@ -1,21 +1,27 @@
1
1
  /**
2
2
  * Debug logging utility
3
3
  * 프로젝트 전역에서 사용할 수 있는 디버그 로깅 함수를 제공합니다.
4
+ * /debug on 설정 시에만 로그가 기록됩니다.
4
5
  */
5
6
 
6
- import { DEBUG_LOG_DIR } from './config.js';
7
+ import { DEBUG_LOG_DIR, isDebugModeEnabled } from './config.js';
7
8
  import { join, dirname } from 'path';
8
9
  import { safeMkdirSync, safeAppendFileSync } from './safe_fs.js';
9
10
 
10
11
  /**
11
12
  * 디버그 로그를 파일에 기록합니다.
12
- * 모든 환경에서 로그를 기록합니다.
13
+ * SHOW_API_PAYLOAD 설정이 true일 때만 로그를 기록합니다.
13
14
  * @param {string} logFileName - 로그 파일 이름 (예: 'ui.log', 'session.log')
14
15
  * @param {string} context - 로그 컨텍스트 (예: 'MainContent', 'AppContext')
15
16
  * @param {string} message - 로그 메시지
16
17
  */
17
18
  export function debugLog(logFileName, context, message) {
18
19
  try {
20
+ // 디버그 모드가 꺼져있으면 로그를 기록하지 않음
21
+ if (!isDebugModeEnabled()) {
22
+ return;
23
+ }
24
+
19
25
  const LOG_FILE = join(DEBUG_LOG_DIR, logFileName);
20
26
  const logDir = dirname(LOG_FILE);
21
27
 
@@ -1,4 +1,6 @@
1
1
  import { uiEvents } from '../system/ui_events.js';
2
+ import { closePersistentShell } from '../system/code_executer.js';
3
+ import { killAllBackgroundProcesses } from '../system/background_process.js';
2
4
 
3
5
  /**
4
6
  * Unified exit handler for the application
@@ -17,6 +19,12 @@ export async function performExit(options = {}) {
17
19
  uiEvents.addSystemMessage(`Goodbye! Session ID: ${sessionID}`);
18
20
  }
19
21
 
22
+ // 백그라운드 프로세스 정리
23
+ await killAllBackgroundProcesses();
24
+
25
+ // PersistentShell 정리
26
+ await closePersistentShell();
27
+
20
28
  // MCP Integration 정리
21
29
  if (mcpIntegration) {
22
30
  await mcpIntegration.cleanup();
@@ -0,0 +1,132 @@
1
+ /**
2
+ * File Reference Parser
3
+ *
4
+ * 메시지에서 @경로 형태의 파일/디렉토리 참조를 파싱하고 변환합니다.
5
+ */
6
+
7
+ import { resolve } from 'path';
8
+ import { safeStat, safeExists } from './safe_fs.js';
9
+
10
+ /**
11
+ * 메시지에서 @참조를 파싱하여 파일/디렉토리 목록과 변환된 메시지를 반환합니다.
12
+ *
13
+ * @param {string} message - 사용자 입력 메시지
14
+ * @returns {Promise<Object>} 파싱 결과
15
+ * @example
16
+ * // 입력: "@src/index.js @package.json 분석해줘"
17
+ * // 출력: {
18
+ * // hasReferences: true,
19
+ * // files: ['src/index.js', 'package.json'],
20
+ * // directories: [],
21
+ * // transformedMessage: "참조된 파일:\n- src/index.js (파일)\n- package.json (파일)\n\n분석해줘"
22
+ * // }
23
+ */
24
+ export async function parseFileReferences(message) {
25
+ if (!message || typeof message !== 'string') {
26
+ return {
27
+ hasReferences: false,
28
+ files: [],
29
+ directories: [],
30
+ transformedMessage: message || ''
31
+ };
32
+ }
33
+
34
+ // @ 뒤에 경로 문자열이 오는 패턴 매칭
35
+ // 경로: 알파벳, 숫자, /, ., _, -, ~로 구성
36
+ const atPattern = /@([^\s@]+)/g;
37
+ const matches = [...message.matchAll(atPattern)];
38
+
39
+ if (matches.length === 0) {
40
+ return {
41
+ hasReferences: false,
42
+ files: [],
43
+ directories: [],
44
+ transformedMessage: message
45
+ };
46
+ }
47
+
48
+ const files = [];
49
+ const directories = [];
50
+ const validReferences = [];
51
+
52
+ // 각 @참조 검증
53
+ for (const match of matches) {
54
+ const refPath = match[1];
55
+
56
+ // 경로 유효성 검사 (최소한의 검증)
57
+ if (!refPath || refPath.length === 0) {
58
+ continue;
59
+ }
60
+
61
+ // 절대 경로로 변환
62
+ const absolutePath = resolve(process.cwd(), refPath);
63
+
64
+ try {
65
+ const exists = await safeExists(absolutePath);
66
+ if (!exists) {
67
+ continue;
68
+ }
69
+
70
+ const stat = await safeStat(absolutePath);
71
+
72
+ if (stat.isDirectory()) {
73
+ directories.push(refPath);
74
+ validReferences.push({
75
+ original: match[0],
76
+ path: refPath,
77
+ type: 'directory'
78
+ });
79
+ } else if (stat.isFile()) {
80
+ files.push(refPath);
81
+ validReferences.push({
82
+ original: match[0],
83
+ path: refPath,
84
+ type: 'file'
85
+ });
86
+ }
87
+ } catch (error) {
88
+ // 존재하지 않거나 접근 불가능한 경로는 무시
89
+ continue;
90
+ }
91
+ }
92
+
93
+ // 유효한 참조가 없으면 원본 메시지 반환
94
+ if (validReferences.length === 0) {
95
+ return {
96
+ hasReferences: false,
97
+ files: [],
98
+ directories: [],
99
+ transformedMessage: message
100
+ };
101
+ }
102
+
103
+ // 메시지에서 @참조 제거
104
+ let cleanedMessage = message;
105
+ for (const ref of validReferences) {
106
+ cleanedMessage = cleanedMessage.replace(ref.original, '').trim();
107
+ }
108
+
109
+ // 연속된 공백 정리
110
+ cleanedMessage = cleanedMessage.replace(/\s+/g, ' ').trim();
111
+
112
+ // 변환된 메시지 생성
113
+ const referenceLines = validReferences.map(ref => {
114
+ const suffix = ref.type === 'directory' ? '/' : '';
115
+ const typeLabel = ref.type === 'directory' ? '디렉토리' : '파일';
116
+ return `- ${ref.path}${suffix} (${typeLabel})`;
117
+ });
118
+
119
+ const transformedMessage = [
120
+ '참조된 파일:',
121
+ ...referenceLines,
122
+ '',
123
+ cleanedMessage
124
+ ].join('\n');
125
+
126
+ return {
127
+ hasReferences: true,
128
+ files,
129
+ directories,
130
+ transformedMessage
131
+ };
132
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * 경로 검증 유틸리티
3
+ * 파일 및 디렉토리 경로의 유효성을 검사하는 통합 유틸리티입니다.
4
+ */
5
+
6
+ import { resolve, isAbsolute, normalize, dirname, basename } from 'path';
7
+ import { safeStat, safeAccess } from './safe_fs.js';
8
+
9
+ /**
10
+ * 경로를 절대경로로 변환합니다
11
+ * @param {string} inputPath - 변환할 경로
12
+ * @returns {string} 절대경로
13
+ */
14
+ export function toAbsolutePath(inputPath) {
15
+ if (!inputPath || typeof inputPath !== 'string') {
16
+ throw new Error('Invalid path: path must be a non-empty string');
17
+ }
18
+
19
+ if (isAbsolute(inputPath)) {
20
+ return normalize(inputPath);
21
+ }
22
+
23
+ return resolve(process.cwd(), inputPath);
24
+ }
25
+
26
+ /**
27
+ * 경로 문자열의 기본 유효성을 검사합니다
28
+ * @param {string} inputPath - 검증할 경로
29
+ * @returns {{valid: boolean, error?: string}} 검증 결과
30
+ */
31
+ export function validatePathString(inputPath) {
32
+ if (!inputPath) {
33
+ return { valid: false, error: 'Path is required' };
34
+ }
35
+
36
+ if (typeof inputPath !== 'string') {
37
+ return { valid: false, error: 'Path must be a string' };
38
+ }
39
+
40
+ if (inputPath.trim() === '') {
41
+ return { valid: false, error: 'Path cannot be empty' };
42
+ }
43
+
44
+ // null 바이트 검사 (보안)
45
+ if (inputPath.includes('\0')) {
46
+ return { valid: false, error: 'Path cannot contain null bytes' };
47
+ }
48
+
49
+ return { valid: true };
50
+ }
51
+
52
+ /**
53
+ * 파일 경로의 유효성을 비동기로 검증합니다
54
+ * @param {string} filePath - 검증할 파일 경로
55
+ * @param {Object} options - 옵션
56
+ * @param {boolean} options.mustExist - 파일이 반드시 존재해야 하는지 여부 (기본: false)
57
+ * @param {boolean} options.allowDirectory - 디렉토리 경로를 허용할지 여부 (기본: false)
58
+ * @returns {Promise<{valid: boolean, absolutePath: string, error?: string}>}
59
+ */
60
+ export async function validateFilePath(filePath, options = {}) {
61
+ const { mustExist = false, allowDirectory = false } = options;
62
+
63
+ // 기본 문자열 검증
64
+ const stringValidation = validatePathString(filePath);
65
+ if (!stringValidation.valid) {
66
+ return { valid: false, absolutePath: null, error: stringValidation.error };
67
+ }
68
+
69
+ // 절대경로 변환
70
+ let absolutePath;
71
+ try {
72
+ absolutePath = toAbsolutePath(filePath);
73
+ } catch (error) {
74
+ return { valid: false, absolutePath: null, error: error.message };
75
+ }
76
+
77
+ // 존재 여부 검사 (필요 시)
78
+ if (mustExist) {
79
+ try {
80
+ const stat = await safeStat(absolutePath);
81
+ if (!allowDirectory && stat.isDirectory()) {
82
+ return { valid: false, absolutePath, error: 'Path is a directory, not a file' };
83
+ }
84
+ } catch (error) {
85
+ return { valid: false, absolutePath, error: `File does not exist: ${absolutePath}` };
86
+ }
87
+ }
88
+
89
+ return { valid: true, absolutePath };
90
+ }
91
+
92
+ /**
93
+ * 디렉토리 경로의 유효성을 비동기로 검증합니다
94
+ * @param {string} dirPath - 검증할 디렉토리 경로
95
+ * @param {Object} options - 옵션
96
+ * @param {boolean} options.mustExist - 디렉토리가 반드시 존재해야 하는지 여부 (기본: false)
97
+ * @returns {Promise<{valid: boolean, absolutePath: string, error?: string}>}
98
+ */
99
+ export async function validateDirectoryPath(dirPath, options = {}) {
100
+ const { mustExist = false } = options;
101
+
102
+ // 기본 문자열 검증
103
+ const stringValidation = validatePathString(dirPath);
104
+ if (!stringValidation.valid) {
105
+ return { valid: false, absolutePath: null, error: stringValidation.error };
106
+ }
107
+
108
+ // 절대경로 변환
109
+ let absolutePath;
110
+ try {
111
+ absolutePath = toAbsolutePath(dirPath);
112
+ } catch (error) {
113
+ return { valid: false, absolutePath: null, error: error.message };
114
+ }
115
+
116
+ // 존재 여부 검사 (필요 시)
117
+ if (mustExist) {
118
+ try {
119
+ const stat = await safeStat(absolutePath);
120
+ if (!stat.isDirectory()) {
121
+ return { valid: false, absolutePath, error: 'Path is not a directory' };
122
+ }
123
+ } catch (error) {
124
+ return { valid: false, absolutePath, error: `Directory does not exist: ${absolutePath}` };
125
+ }
126
+ }
127
+
128
+ return { valid: true, absolutePath };
129
+ }
130
+
131
+ /**
132
+ * 경로가 특정 디렉토리 내에 있는지 검사합니다
133
+ * @param {string} targetPath - 검사할 경로
134
+ * @param {string} basePath - 기준 디렉토리 경로
135
+ * @returns {boolean} 기준 디렉토리 내에 있으면 true
136
+ */
137
+ export function isWithinDirectory(targetPath, basePath) {
138
+ const normalizedTarget = normalize(resolve(targetPath));
139
+ const normalizedBase = normalize(resolve(basePath));
140
+
141
+ return normalizedTarget.startsWith(normalizedBase + '/') ||
142
+ normalizedTarget === normalizedBase;
143
+ }
144
+
145
+ /**
146
+ * 파일 확장자를 검증합니다
147
+ * @param {string} filePath - 파일 경로
148
+ * @param {string[]} allowedExtensions - 허용된 확장자 목록 (예: ['.js', '.ts'])
149
+ * @returns {boolean} 허용된 확장자이면 true
150
+ */
151
+ export function hasAllowedExtension(filePath, allowedExtensions) {
152
+ if (!filePath || !Array.isArray(allowedExtensions)) {
153
+ return false;
154
+ }
155
+
156
+ const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase();
157
+ return allowedExtensions.some(allowed =>
158
+ allowed.toLowerCase() === ext
159
+ );
160
+ }
161
+
162
+ /**
163
+ * 경로에서 파일명 추출
164
+ * @param {string} filePath - 파일 경로
165
+ * @returns {string} 파일명
166
+ */
167
+ export function getFileName(filePath) {
168
+ return basename(filePath);
169
+ }
170
+
171
+ /**
172
+ * 경로에서 디렉토리 추출
173
+ * @param {string} filePath - 파일 경로
174
+ * @returns {string} 디렉토리 경로
175
+ */
176
+ export function getDirectory(filePath) {
177
+ return dirname(filePath);
178
+ }