aiexecode 1.0.94 → 1.0.96
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.
Potentially problematic release.
This version of aiexecode might be problematic. Click here for more details.
- package/README.md +210 -87
- package/index.js +33 -1
- package/package.json +3 -3
- package/payload_viewer/out/404/index.html +1 -1
- package/payload_viewer/out/404.html +1 -1
- package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
- package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
- package/payload_viewer/out/index.html +1 -1
- package/payload_viewer/out/index.txt +3 -3
- package/payload_viewer/web_server.js +361 -0
- package/src/LLMClient/client.js +392 -16
- package/src/LLMClient/converters/responses-to-claude.js +67 -18
- package/src/LLMClient/converters/responses-to-zai.js +608 -0
- package/src/LLMClient/errors.js +18 -4
- package/src/LLMClient/index.js +5 -0
- package/src/ai_based/completion_judge.js +35 -4
- package/src/ai_based/orchestrator.js +146 -35
- package/src/commands/agents.js +70 -0
- package/src/commands/commands.js +51 -0
- package/src/commands/debug.js +52 -0
- package/src/commands/help.js +11 -1
- package/src/commands/model.js +43 -7
- package/src/commands/skills.js +46 -0
- package/src/config/ai_models.js +96 -5
- package/src/config/constants.js +71 -0
- package/src/frontend/components/HelpView.js +106 -2
- package/src/frontend/components/SetupWizard.js +53 -8
- package/src/frontend/utils/toolUIFormatter.js +261 -0
- package/src/system/agents_loader.js +289 -0
- package/src/system/ai_request.js +147 -9
- package/src/system/command_parser.js +33 -3
- package/src/system/conversation_state.js +265 -0
- package/src/system/custom_command_loader.js +386 -0
- package/src/system/session.js +59 -35
- package/src/system/skill_loader.js +318 -0
- package/src/system/tool_approval.js +10 -0
- package/src/tools/file_reader.js +49 -9
- package/src/tools/glob.js +0 -3
- package/src/tools/ripgrep.js +5 -7
- package/src/tools/skill_tool.js +122 -0
- package/src/tools/web_downloader.js +0 -3
- package/src/util/clone.js +174 -0
- package/src/util/config.js +38 -2
- package/src/util/config_migration.js +174 -0
- package/src/util/path_validator.js +178 -0
- package/src/util/prompt_loader.js +68 -1
- package/src/util/safe_fs.js +43 -3
- package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → lHmNygVpv4N1VR0LdnwkJ}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → lHmNygVpv4N1VR0LdnwkJ}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → lHmNygVpv4N1VR0LdnwkJ}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 깊은 복사 유틸리티
|
|
3
|
+
* JSON.parse(JSON.stringify()) 패턴을 대체하는 안전한 깊은 복사 함수들을 제공합니다.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 값을 깊은 복사합니다.
|
|
8
|
+
* - 순환 참조를 감지하고 처리합니다.
|
|
9
|
+
* - Date, RegExp 등의 특수 객체를 올바르게 복사합니다.
|
|
10
|
+
* - undefined, function, Symbol은 복사되지 않습니다 (JSON.stringify와 동일).
|
|
11
|
+
*
|
|
12
|
+
* @param {*} value - 복사할 값
|
|
13
|
+
* @param {WeakMap} [visited] - 순환 참조 감지용 (내부 사용)
|
|
14
|
+
* @returns {*} 깊은 복사된 값
|
|
15
|
+
*/
|
|
16
|
+
export function deepClone(value, visited = new WeakMap()) {
|
|
17
|
+
// 기본 타입은 그대로 반환
|
|
18
|
+
if (value === null || typeof value !== 'object') {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 순환 참조 감지
|
|
23
|
+
if (visited.has(value)) {
|
|
24
|
+
return visited.get(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Date 객체 처리
|
|
28
|
+
if (value instanceof Date) {
|
|
29
|
+
return new Date(value.getTime());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// RegExp 객체 처리
|
|
33
|
+
if (value instanceof RegExp) {
|
|
34
|
+
return new RegExp(value.source, value.flags);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Map 객체 처리
|
|
38
|
+
if (value instanceof Map) {
|
|
39
|
+
const clonedMap = new Map();
|
|
40
|
+
visited.set(value, clonedMap);
|
|
41
|
+
for (const [key, val] of value) {
|
|
42
|
+
clonedMap.set(deepClone(key, visited), deepClone(val, visited));
|
|
43
|
+
}
|
|
44
|
+
return clonedMap;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Set 객체 처리
|
|
48
|
+
if (value instanceof Set) {
|
|
49
|
+
const clonedSet = new Set();
|
|
50
|
+
visited.set(value, clonedSet);
|
|
51
|
+
for (const val of value) {
|
|
52
|
+
clonedSet.add(deepClone(val, visited));
|
|
53
|
+
}
|
|
54
|
+
return clonedSet;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 배열 처리
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
const clonedArray = [];
|
|
60
|
+
visited.set(value, clonedArray);
|
|
61
|
+
for (let i = 0; i < value.length; i++) {
|
|
62
|
+
clonedArray[i] = deepClone(value[i], visited);
|
|
63
|
+
}
|
|
64
|
+
return clonedArray;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 일반 객체 처리
|
|
68
|
+
const clonedObj = {};
|
|
69
|
+
visited.set(value, clonedObj);
|
|
70
|
+
|
|
71
|
+
for (const key of Object.keys(value)) {
|
|
72
|
+
const val = value[key];
|
|
73
|
+
// undefined와 function은 스킵 (JSON.stringify 동작과 일치)
|
|
74
|
+
if (val !== undefined && typeof val !== 'function') {
|
|
75
|
+
clonedObj[key] = deepClone(val, visited);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return clonedObj;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* JSON 직렬화 가능한 값만 깊은 복사합니다.
|
|
84
|
+
* JSON.parse(JSON.stringify())와 동일한 동작이지만 에러 처리가 추가되었습니다.
|
|
85
|
+
*
|
|
86
|
+
* @param {*} value - 복사할 값
|
|
87
|
+
* @param {*} [fallback] - 복사 실패 시 반환할 기본값 (기본: null)
|
|
88
|
+
* @returns {*} 깊은 복사된 값 또는 fallback
|
|
89
|
+
*/
|
|
90
|
+
export function jsonClone(value, fallback = null) {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(JSON.stringify(value));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 얕은 복사를 수행합니다.
|
|
100
|
+
* 첫 번째 레벨만 복사하고 중첩된 객체는 참조를 유지합니다.
|
|
101
|
+
*
|
|
102
|
+
* @param {*} value - 복사할 값
|
|
103
|
+
* @returns {*} 얕은 복사된 값
|
|
104
|
+
*/
|
|
105
|
+
export function shallowClone(value) {
|
|
106
|
+
if (value === null || typeof value !== 'object') {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
return [...value];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { ...value };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 두 객체를 깊은 병합합니다.
|
|
119
|
+
* target에 source의 값을 병합합니다 (source가 우선).
|
|
120
|
+
*
|
|
121
|
+
* @param {Object} target - 대상 객체
|
|
122
|
+
* @param {Object} source - 소스 객체
|
|
123
|
+
* @returns {Object} 병합된 새 객체
|
|
124
|
+
*/
|
|
125
|
+
export function deepMerge(target, source) {
|
|
126
|
+
if (source === null || typeof source !== 'object') {
|
|
127
|
+
return source;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (target === null || typeof target !== 'object') {
|
|
131
|
+
return deepClone(source);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (Array.isArray(source)) {
|
|
135
|
+
return deepClone(source);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = { ...target };
|
|
139
|
+
|
|
140
|
+
for (const key of Object.keys(source)) {
|
|
141
|
+
const sourceValue = source[key];
|
|
142
|
+
const targetValue = target[key];
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
sourceValue !== null &&
|
|
146
|
+
typeof sourceValue === 'object' &&
|
|
147
|
+
!Array.isArray(sourceValue) &&
|
|
148
|
+
targetValue !== null &&
|
|
149
|
+
typeof targetValue === 'object' &&
|
|
150
|
+
!Array.isArray(targetValue)
|
|
151
|
+
) {
|
|
152
|
+
result[key] = deepMerge(targetValue, sourceValue);
|
|
153
|
+
} else {
|
|
154
|
+
result[key] = deepClone(sourceValue);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 객체가 깊은 복사 가능한지 (JSON 직렬화 가능한지) 검사합니다.
|
|
163
|
+
*
|
|
164
|
+
* @param {*} value - 검사할 값
|
|
165
|
+
* @returns {boolean} JSON 직렬화 가능하면 true
|
|
166
|
+
*/
|
|
167
|
+
export function isCloneable(value) {
|
|
168
|
+
try {
|
|
169
|
+
JSON.stringify(value);
|
|
170
|
+
return true;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/util/config.js
CHANGED
|
@@ -41,10 +41,20 @@ export const PAYLOAD_LOG_DIR = join(CONFIG_DIR, 'payload_log');
|
|
|
41
41
|
export const PAYLOAD_LLM_LOG_DIR = join(CONFIG_DIR, 'payload_LLM_log');
|
|
42
42
|
export const DEBUG_LOG_DIR = join(CONFIG_DIR, 'debuglog');
|
|
43
43
|
export const DEBUG_LOG_FILE = join(CONFIG_DIR, 'debug.txt'); // Deprecated: 호환성을 위해 유지
|
|
44
|
+
// Skill 시스템 경로
|
|
45
|
+
export const PERSONAL_SKILLS_DIR = join(CONFIG_DIR, 'skills'); // ~/.aiexe/skills/
|
|
46
|
+
export const PROJECT_SKILLS_DIR = '.aiexe/skills'; // CWD/.aiexe/skills/ (상대경로)
|
|
47
|
+
|
|
48
|
+
// Custom Commands 시스템 경로 (Claude Code 스타일)
|
|
49
|
+
export const PERSONAL_COMMANDS_DIR = join(CONFIG_DIR, 'commands'); // ~/.aiexe/commands/
|
|
50
|
+
export const PROJECT_COMMANDS_DIR = '.aiexe/commands'; // CWD/.aiexe/commands/ (상대경로)
|
|
44
51
|
const DEFAULT_SETTINGS = {
|
|
45
52
|
API_KEY: '',
|
|
46
53
|
MODEL: DEFAULT_MODEL,
|
|
54
|
+
BASE_URL: '', // 커스텀 API 엔드포인트 (비어있으면 기본값 사용)
|
|
47
55
|
REASONING_EFFORT: 'medium', // 'minimal', 'low', 'medium', 'high'
|
|
56
|
+
// API 요청/응답 내용 화면 표시 여부
|
|
57
|
+
SHOW_API_PAYLOAD: false, // true: 요청/응답 내용을 화면에 표시, false: 표시하지 않음
|
|
48
58
|
// 도구 활성화 옵션
|
|
49
59
|
TOOLS_ENABLED: {
|
|
50
60
|
edit_file_range: false, // 기본적으로 비활성화 (edit_file_replace 사용 권장)
|
|
@@ -74,7 +84,7 @@ export async function ensureConfigDirectory() {
|
|
|
74
84
|
if (!templateDir) return;
|
|
75
85
|
|
|
76
86
|
const templateFiles = await safeReaddir(templateDir);
|
|
77
|
-
await Promise.
|
|
87
|
+
const copyResults = await Promise.allSettled(
|
|
78
88
|
templateFiles.map(async (fileName) => {
|
|
79
89
|
const sourcePath = join(templateDir, fileName);
|
|
80
90
|
const stat = await safeStat(sourcePath);
|
|
@@ -82,8 +92,15 @@ export async function ensureConfigDirectory() {
|
|
|
82
92
|
const destPath = join(CONFIG_DIR, fileName);
|
|
83
93
|
await safeCopyFile(sourcePath, destPath);
|
|
84
94
|
}
|
|
95
|
+
return fileName;
|
|
85
96
|
})
|
|
86
97
|
);
|
|
98
|
+
// 실패한 복사 작업은 조용히 무시 (설정 디렉토리 초기화 실패가 앱 전체를 막으면 안됨)
|
|
99
|
+
const failedCopies = copyResults.filter(r => r.status === 'rejected');
|
|
100
|
+
if (failedCopies.length > 0) {
|
|
101
|
+
// 로깅만 하고 에러는 던지지 않음
|
|
102
|
+
console.error(`[config] ${failedCopies.length} template file(s) failed to copy`);
|
|
103
|
+
}
|
|
87
104
|
} catch (error) {
|
|
88
105
|
// Failed to ensure config directory - silently ignore
|
|
89
106
|
throw error;
|
|
@@ -131,7 +148,7 @@ export async function saveSettings(settings) {
|
|
|
131
148
|
/**
|
|
132
149
|
* API 키의 접두어를 기반으로 발급처를 판단합니다.
|
|
133
150
|
* @param {string} apiKey - API 키 문자열
|
|
134
|
-
* @returns {string|null} 발급처 이름 ("Google", "OpenAI", "Anthropic") 또는 null (알 수 없는 경우)
|
|
151
|
+
* @returns {string|null} 발급처 이름 ("Google", "OpenAI", "Anthropic", "Z.AI") 또는 null (알 수 없는 경우)
|
|
135
152
|
*/
|
|
136
153
|
export function APIKeyIssuedFrom(apiKey) {
|
|
137
154
|
if (!apiKey || typeof apiKey !== 'string') {
|
|
@@ -144,7 +161,26 @@ export function APIKeyIssuedFrom(apiKey) {
|
|
|
144
161
|
return 'OpenAI';
|
|
145
162
|
} else if (apiKey.startsWith('sk-ant-')) {
|
|
146
163
|
return 'Anthropic';
|
|
164
|
+
} else if (/^[a-f0-9]{32}\.[A-Za-z0-9]{16}$/.test(apiKey)) {
|
|
165
|
+
// Z.AI API 키 형식: 32자리 hex + '.' + 16자리 영숫자
|
|
166
|
+
return 'Z.AI';
|
|
147
167
|
}
|
|
148
168
|
|
|
149
169
|
return null;
|
|
150
170
|
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Provider별 기본 BASE_URL 반환
|
|
174
|
+
* @param {string} provider - 프로바이더 이름 ('openai', 'claude', 'zai', 'gemini')
|
|
175
|
+
* @returns {string|null} 기본 BASE_URL 또는 null
|
|
176
|
+
*/
|
|
177
|
+
export function getDefaultBaseUrlForProvider(provider) {
|
|
178
|
+
const baseUrls = {
|
|
179
|
+
'zai': 'https://api.z.ai/api/anthropic',
|
|
180
|
+
// 다른 프로바이더는 SDK 기본값 사용
|
|
181
|
+
'openai': null,
|
|
182
|
+
'claude': null,
|
|
183
|
+
'gemini': null
|
|
184
|
+
};
|
|
185
|
+
return baseUrls[provider] || null;
|
|
186
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -2,6 +2,7 @@ import { safeReadFile, safeAccess } from "./safe_fs.js";
|
|
|
2
2
|
import { join, dirname } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import ejs from "ejs";
|
|
5
|
+
import { supportsCaching } from "../config/ai_models.js";
|
|
5
6
|
|
|
6
7
|
const moduleDirname = dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const defaultProjectRoot = dirname(dirname(moduleDirname));
|
|
@@ -67,9 +68,12 @@ async function loadPromptFromPromptsDir(promptFileName) {
|
|
|
67
68
|
* 프롬프트를 로드하여 시스템 메시지 객체를 생성합니다.
|
|
68
69
|
* @param {string} promptFileName - 프롬프트 파일명 (예: "verifier.txt")
|
|
69
70
|
* @param {Object} [templateVars] - 템플릿 변수 객체 (예: { what_user_requests: "print hello world" })
|
|
71
|
+
* @param {Object} [options] - 추가 옵션
|
|
72
|
+
* @param {string} [options.model] - 현재 사용 중인 모델 (캐시 제어용)
|
|
73
|
+
* @param {boolean} [options.enableCache] - 캐시 제어 활성화 여부
|
|
70
74
|
* @returns {Object} 시스템 메시지 객체 { role: "system", content: string }
|
|
71
75
|
*/
|
|
72
|
-
export async function createSystemMessage(promptFileName, templateVars = {}) {
|
|
76
|
+
export async function createSystemMessage(promptFileName, templateVars = {}, options = {}) {
|
|
73
77
|
let content = await loadPromptFromPromptsDir(promptFileName);
|
|
74
78
|
|
|
75
79
|
// EJS 템플릿 엔진을 사용하여 렌더링
|
|
@@ -87,8 +91,71 @@ export async function createSystemMessage(promptFileName, templateVars = {}) {
|
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
// 캐시 제어 활성화 여부 확인
|
|
95
|
+
const { model, enableCache = false } = options;
|
|
96
|
+
const shouldCache = enableCache && model && supportsCaching(model);
|
|
97
|
+
|
|
98
|
+
if (shouldCache) {
|
|
99
|
+
// Z.AI/GLM 등 캐싱을 지원하는 모델의 경우 cache_control 추가
|
|
100
|
+
return {
|
|
101
|
+
role: "system",
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: content,
|
|
106
|
+
cache_control: { type: "ephemeral" }
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
90
112
|
return {
|
|
91
113
|
role: "system",
|
|
92
114
|
content: content
|
|
93
115
|
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* System-Reminder 메시지를 생성합니다.
|
|
120
|
+
* Claude Code 스타일의 시스템 알림으로, 사용자에게는 표시하지 않도록 지시됩니다.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} reminderText - 리마인더 내용
|
|
123
|
+
* @param {Object} [options] - 추가 옵션
|
|
124
|
+
* @param {boolean} [options.hideFromUser] - 사용자에게 언급하지 말라는 지시 추가 (기본: true)
|
|
125
|
+
* @returns {string} system-reminder 태그로 감싼 텍스트
|
|
126
|
+
*/
|
|
127
|
+
export function createSystemReminder(reminderText, options = {}) {
|
|
128
|
+
const { hideFromUser = true } = options;
|
|
129
|
+
|
|
130
|
+
let reminder = reminderText;
|
|
131
|
+
if (hideFromUser) {
|
|
132
|
+
reminder += '\n이 메시지를 사용자에게 명시적으로 언급하지 마세요.';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return `<system-reminder>\n${reminder}\n</system-reminder>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 할 일 목록 상태 알림을 생성합니다.
|
|
140
|
+
* @param {Array} todos - 현재 할 일 목록
|
|
141
|
+
* @returns {string} system-reminder 형식의 알림
|
|
142
|
+
*/
|
|
143
|
+
export function createTodoReminder(todos) {
|
|
144
|
+
if (!todos || todos.length === 0) {
|
|
145
|
+
return createSystemReminder(
|
|
146
|
+
'현재 할 일 목록이 비어 있습니다. 할 일 목록이 도움이 될 작업을 진행 중이라면 TodoWrite 도구를 사용해 목록을 생성하세요.',
|
|
147
|
+
{ hideFromUser: true }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const todoList = todos.map((todo, index) => {
|
|
152
|
+
const statusIcon = todo.status === 'completed' ? '✓' :
|
|
153
|
+
todo.status === 'in_progress' ? '→' : '○';
|
|
154
|
+
return `${index + 1}. [${statusIcon}] ${todo.content} (${todo.status})`;
|
|
155
|
+
}).join('\n');
|
|
156
|
+
|
|
157
|
+
return createSystemReminder(
|
|
158
|
+
`현재 할 일 목록:\n${todoList}\n\n완료된 작업은 즉시 완료 처리하세요. 한 번에 하나의 작업만 in_progress로 유지하세요.`,
|
|
159
|
+
{ hideFromUser: false }
|
|
160
|
+
);
|
|
94
161
|
}
|