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.
- package/README.md +198 -88
- package/index.js +310 -86
- package/mcp-agent-lib/src/mcp_message_logger.js +17 -16
- package/package.json +4 -4
- 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/prompts/completion_judge.txt +4 -0
- package/prompts/orchestrator.txt +116 -3
- package/src/LLMClient/client.js +401 -18
- package/src/LLMClient/converters/responses-to-claude.js +67 -18
- package/src/LLMClient/converters/responses-to-zai.js +667 -0
- package/src/LLMClient/errors.js +30 -4
- package/src/LLMClient/index.js +5 -0
- package/src/ai_based/completion_judge.js +263 -186
- package/src/ai_based/orchestrator.js +171 -35
- package/src/commands/agents.js +70 -0
- package/src/commands/apikey.js +1 -1
- package/src/commands/bg.js +129 -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 +42 -7
- package/src/commands/reasoning_effort.js +2 -2
- package/src/commands/skills.js +46 -0
- package/src/config/ai_models.js +106 -6
- package/src/config/constants.js +71 -0
- package/src/config/feature_flags.js +6 -7
- package/src/frontend/App.js +108 -1
- package/src/frontend/components/AutocompleteMenu.js +7 -1
- package/src/frontend/components/BackgroundProcessList.js +175 -0
- package/src/frontend/components/ConversationItem.js +26 -10
- package/src/frontend/components/CurrentModelView.js +2 -2
- package/src/frontend/components/HelpView.js +106 -2
- package/src/frontend/components/Input.js +33 -11
- package/src/frontend/components/ModelListView.js +1 -1
- package/src/frontend/components/SetupWizard.js +51 -8
- package/src/frontend/hooks/useFileCompletion.js +467 -0
- package/src/frontend/utils/toolUIFormatter.js +261 -0
- package/src/system/agents_loader.js +289 -0
- package/src/system/ai_request.js +156 -12
- package/src/system/background_process.js +317 -0
- package/src/system/code_executer.js +496 -56
- package/src/system/command_parser.js +33 -3
- package/src/system/conversation_state.js +265 -0
- package/src/system/conversation_trimmer.js +132 -0
- package/src/system/custom_command_loader.js +386 -0
- package/src/system/file_integrity.js +73 -10
- package/src/system/log.js +10 -2
- package/src/system/output_helper.js +52 -9
- package/src/system/session.js +213 -58
- package/src/system/session_memory.js +30 -2
- package/src/system/skill_loader.js +318 -0
- package/src/system/system_info.js +254 -40
- package/src/system/tool_approval.js +10 -0
- package/src/system/tool_registry.js +15 -1
- package/src/system/ui_events.js +11 -0
- package/src/tools/code_editor.js +16 -10
- package/src/tools/file_reader.js +66 -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 +55 -2
- package/src/util/config_migration.js +174 -0
- package/src/util/debug_log.js +8 -2
- package/src/util/exit_handler.js +8 -0
- package/src/util/file_reference_parser.js +132 -0
- package/src/util/path_validator.js +178 -0
- package/src/util/prompt_loader.js +91 -1
- package/src/util/safe_fs.js +66 -3
- package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_ssgManifest.js +0 -0
|
@@ -102,6 +102,11 @@ async function willDefinitelyFail(toolName, args) {
|
|
|
102
102
|
* @returns {Promise<boolean|{skipApproval: true, reason: string}>} 승인 필요 여부 또는 스킵 정보
|
|
103
103
|
*/
|
|
104
104
|
export async function requiresApproval(toolName, args = {}) {
|
|
105
|
+
// --dangerously-skip-permissions 플래그가 설정된 경우 모든 승인 건너뛰기
|
|
106
|
+
if (process.app_custom?.dangerouslySkipPermissions) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
105
110
|
if (alwaysAllowedTools.has(toolName)) {
|
|
106
111
|
return false;
|
|
107
112
|
}
|
|
@@ -123,6 +128,11 @@ export async function requiresApproval(toolName, args = {}) {
|
|
|
123
128
|
* 사용자에게 도구 실행 승인 요청
|
|
124
129
|
*/
|
|
125
130
|
export async function requestApproval(toolName, args) {
|
|
131
|
+
// --dangerously-skip-permissions 플래그가 설정된 경우 즉시 승인
|
|
132
|
+
if (process.app_custom?.dangerouslySkipPermissions) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
// 항상 허용된 도구는 즉시 승인
|
|
127
137
|
if (alwaysAllowedTools.has(toolName)) {
|
|
128
138
|
return true;
|
|
@@ -11,6 +11,19 @@ import { RESPONSE_MESSAGE_FUNCTIONS, responseMessageSchema } from '../tools/resp
|
|
|
11
11
|
import { TODO_WRITE_FUNCTIONS, todoWriteSchema } from '../tools/todo_write.js';
|
|
12
12
|
import { runPythonCodeSchema, bashSchema } from '../system/code_executer.js';
|
|
13
13
|
|
|
14
|
+
// invoke_skill UI 표시 설정
|
|
15
|
+
const invokeSkillDisplayConfig = {
|
|
16
|
+
ui_display: {
|
|
17
|
+
show_tool_call: true,
|
|
18
|
+
show_tool_result: false,
|
|
19
|
+
display_name: 'Skill',
|
|
20
|
+
format_tool_call: (args) => {
|
|
21
|
+
const skillName = args?.skill_name || 'unknown';
|
|
22
|
+
return `(${skillName})`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
14
27
|
// 도구 스키마 레지스트리
|
|
15
28
|
const TOOL_SCHEMAS = {
|
|
16
29
|
'response_message': responseMessageSchema,
|
|
@@ -24,7 +37,8 @@ const TOOL_SCHEMAS = {
|
|
|
24
37
|
'ripgrep': ripgrepSchema,
|
|
25
38
|
'run_python_code': runPythonCodeSchema,
|
|
26
39
|
'bash': bashSchema,
|
|
27
|
-
'todo_write': todoWriteSchema
|
|
40
|
+
'todo_write': todoWriteSchema,
|
|
41
|
+
'invoke_skill': invokeSkillDisplayConfig
|
|
28
42
|
};
|
|
29
43
|
|
|
30
44
|
/**
|
package/src/system/ui_events.js
CHANGED
|
@@ -45,6 +45,17 @@ class UIEventEmitter extends EventEmitter {
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* 스킬 실행 알림
|
|
50
|
+
*/
|
|
51
|
+
addSkillInvoked(skillName) {
|
|
52
|
+
this.emit('history:add', {
|
|
53
|
+
type: 'skill_invoked',
|
|
54
|
+
text: `/${skillName}`,
|
|
55
|
+
timestamp: Date.now()
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
/**
|
|
49
60
|
* 도구 실행 시작
|
|
50
61
|
*/
|
package/src/tools/code_editor.js
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname, resolve, join } from 'path';
|
|
|
3
3
|
import * as diff from 'diff';
|
|
4
4
|
import { assertFileIntegrity, trackFileRead, saveFileSnapshot } from '../system/file_integrity.js';
|
|
5
5
|
import { createDebugLogger } from '../util/debug_log.js';
|
|
6
|
-
import { DEBUG_LOG_DIR } from '../util/config.js';
|
|
6
|
+
import { DEBUG_LOG_DIR, isDebugModeEnabled } from '../util/config.js';
|
|
7
7
|
import { toDisplayPath } from '../util/path_helper.js';
|
|
8
8
|
import { theme } from '../frontend/design/themeColors.js';
|
|
9
9
|
|
|
@@ -105,9 +105,11 @@ export async function write_file({ file_path, content }) {
|
|
|
105
105
|
} catch (integrityError) {
|
|
106
106
|
internalDebugLog.push(`[${timestamp}] assertFileIntegrity FAILED: ${integrityError.message}`);
|
|
107
107
|
debugLog(`ERROR: assertFileIntegrity FAILED: ${integrityError.message}`);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
if (isDebugModeEnabled()) {
|
|
109
|
+
const logFile = getLogFile();
|
|
110
|
+
await safeMkdir(dirname(logFile), { recursive: true }).catch(() => {});
|
|
111
|
+
await safeAppendFile(logFile, internalDebugLog.join('\n') + '\n');
|
|
112
|
+
}
|
|
111
113
|
throw integrityError;
|
|
112
114
|
}
|
|
113
115
|
}
|
|
@@ -130,9 +132,11 @@ export async function write_file({ file_path, content }) {
|
|
|
130
132
|
saveFileSnapshot(absolutePath, content);
|
|
131
133
|
debugLog(`Snapshot saved`);
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
if (isDebugModeEnabled()) {
|
|
136
|
+
const logFile = getLogFile();
|
|
137
|
+
await safeMkdir(dirname(logFile), { recursive: true }).catch(() => {});
|
|
138
|
+
await safeAppendFile(logFile, internalDebugLog.join('\n') + '\n');
|
|
139
|
+
}
|
|
136
140
|
|
|
137
141
|
// diff 생성 (기존 파일이 있었을 경우에만, 절대경로 사용)
|
|
138
142
|
let diffInfo = null;
|
|
@@ -176,9 +180,11 @@ export async function write_file({ file_path, content }) {
|
|
|
176
180
|
debugLog(`Exception caught: ${error.message}`);
|
|
177
181
|
debugLog(`Stack trace: ${error.stack}`);
|
|
178
182
|
debugLog('========== write_file EXCEPTION END ==========');
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
if (isDebugModeEnabled()) {
|
|
184
|
+
const logFile = getLogFile();
|
|
185
|
+
await safeMkdir(dirname(logFile), { recursive: true }).catch(() => {});
|
|
186
|
+
await safeAppendFile(logFile, internalDebugLog.join('\n') + '\n').catch(() => {});
|
|
187
|
+
}
|
|
182
188
|
|
|
183
189
|
// 에러 시에도 절대경로로 반환
|
|
184
190
|
const absolutePath = resolve(file_path);
|
package/src/tools/file_reader.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { safeReadFile } from '../util/safe_fs.js';
|
|
1
|
+
import { safeReadFile, safeStat } from '../util/safe_fs.js';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import { createHash } from 'crypto';
|
|
4
4
|
import { trackFileRead, saveFileSnapshot } from '../system/file_integrity.js';
|
|
5
|
+
import { markFileAsReRead } from '../system/conversation_trimmer.js';
|
|
5
6
|
import { createDebugLogger } from '../util/debug_log.js';
|
|
6
7
|
import { toDisplayPath } from '../util/path_helper.js';
|
|
7
8
|
import { theme } from '../frontend/design/themeColors.js';
|
|
9
|
+
import {
|
|
10
|
+
FILE_READER_MAX_LINES as MAX_LINES,
|
|
11
|
+
FILE_READER_MAX_SIZE_BYTES as MAX_FILE_SIZE_BYTES
|
|
12
|
+
} from '../config/constants.js';
|
|
8
13
|
|
|
9
14
|
const debugLog = createDebugLogger('file_reader.log', 'file_reader');
|
|
10
15
|
|
|
@@ -32,9 +37,6 @@ export async function read_file({ filePath }) {
|
|
|
32
37
|
debugLog(` - filePath starts with '../': ${filePath?.startsWith('../') || false}`);
|
|
33
38
|
debugLog(` - Current Working Directory: ${process.cwd()}`);
|
|
34
39
|
|
|
35
|
-
// Intentional delay for testing pending state
|
|
36
|
-
await new Promise(resolve => setTimeout(resolve, 13));
|
|
37
|
-
|
|
38
40
|
try {
|
|
39
41
|
// 경로를 절대경로로 정규화
|
|
40
42
|
const absolutePath = resolve(filePath);
|
|
@@ -45,6 +47,30 @@ export async function read_file({ filePath }) {
|
|
|
45
47
|
debugLog(` - Absolute path starts with '/': ${absolutePath.startsWith('/')}`);
|
|
46
48
|
debugLog(` - Absolute path length: ${absolutePath.length}`);
|
|
47
49
|
|
|
50
|
+
// 파일 크기 제한 검사 (보안)
|
|
51
|
+
debugLog(`Checking file size...`);
|
|
52
|
+
try {
|
|
53
|
+
const stat = await safeStat(absolutePath);
|
|
54
|
+
const fileSizeBytes = stat.size;
|
|
55
|
+
debugLog(`File size: ${fileSizeBytes} bytes (${(fileSizeBytes / 1024 / 1024).toFixed(2)} MB)`);
|
|
56
|
+
|
|
57
|
+
if (fileSizeBytes > MAX_FILE_SIZE_BYTES) {
|
|
58
|
+
debugLog(`ERROR: File exceeds ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB size limit`);
|
|
59
|
+
debugLog('========== read_file ERROR END ==========');
|
|
60
|
+
// trim된 파일 목록에서 제거 (재읽기 시도했으므로 알림 불필요)
|
|
61
|
+
markFileAsReRead(absolutePath);
|
|
62
|
+
return {
|
|
63
|
+
operation_successful: false,
|
|
64
|
+
error_message: `File exceeds 10MB size limit (actual: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB). Large files cannot be read for security reasons.`,
|
|
65
|
+
target_file_path: absolutePath,
|
|
66
|
+
file_size_bytes: fileSizeBytes
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
} catch (statError) {
|
|
70
|
+
// stat 실패 시 파일이 존재하지 않을 수 있음 - 읽기에서 처리
|
|
71
|
+
debugLog(`Stat failed (file may not exist): ${statError.message}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
48
74
|
debugLog(`Reading file...`);
|
|
49
75
|
const content = await safeReadFile(absolutePath, 'utf8');
|
|
50
76
|
debugLog(`File read successful: ${content.length} bytes`);
|
|
@@ -56,11 +82,12 @@ export async function read_file({ filePath }) {
|
|
|
56
82
|
(content.endsWith('\n') ? lines.length - 1 : lines.length);
|
|
57
83
|
debugLog(`Total lines: ${totalLines}`);
|
|
58
84
|
|
|
59
|
-
//
|
|
60
|
-
const MAX_LINES = 2000;
|
|
85
|
+
// 줄 수 제한 체크
|
|
61
86
|
if (totalLines > MAX_LINES) {
|
|
62
87
|
debugLog(`ERROR: File exceeds ${MAX_LINES} lines limit`);
|
|
63
88
|
debugLog('========== read_file ERROR END ==========');
|
|
89
|
+
// trim된 파일 목록에서 제거 (재읽기 시도했으므로 알림 불필요)
|
|
90
|
+
markFileAsReRead(absolutePath);
|
|
64
91
|
return {
|
|
65
92
|
operation_successful: false,
|
|
66
93
|
error_message: `File exceeds ${MAX_LINES} lines (actual: ${totalLines} lines). Use read_file_range to read specific sections of this large file.`,
|
|
@@ -76,6 +103,9 @@ export async function read_file({ filePath }) {
|
|
|
76
103
|
await trackFileRead(absolutePath, content);
|
|
77
104
|
debugLog(`File read tracked`);
|
|
78
105
|
|
|
106
|
+
// trim된 파일 목록에서 제거 (다시 읽었으므로 알림 불필요)
|
|
107
|
+
markFileAsReRead(absolutePath);
|
|
108
|
+
|
|
79
109
|
// 스냅샷 저장 (UI 미리보기용)
|
|
80
110
|
debugLog(`Saving file snapshot...`);
|
|
81
111
|
saveFileSnapshot(absolutePath, content);
|
|
@@ -103,6 +133,8 @@ export async function read_file({ filePath }) {
|
|
|
103
133
|
|
|
104
134
|
// 에러 시에도 절대경로로 반환
|
|
105
135
|
const absolutePath = resolve(filePath);
|
|
136
|
+
// trim된 파일 목록에서 제거 (재읽기 시도했으므로 알림 불필요)
|
|
137
|
+
markFileAsReRead(absolutePath);
|
|
106
138
|
return {
|
|
107
139
|
operation_successful: false,
|
|
108
140
|
error_message: error.message,
|
|
@@ -172,9 +204,6 @@ export async function read_file_range({ filePath, startLine, endLine }) {
|
|
|
172
204
|
debugLog(` endLine: ${endLine}`);
|
|
173
205
|
debugLog(` - Current Working Directory: ${process.cwd()}`);
|
|
174
206
|
|
|
175
|
-
// Intentional delay for testing pending state
|
|
176
|
-
await new Promise(resolve => setTimeout(resolve, 13));
|
|
177
|
-
|
|
178
207
|
try {
|
|
179
208
|
// 경로를 절대경로로 정규화
|
|
180
209
|
const absolutePath = resolve(filePath);
|
|
@@ -185,6 +214,29 @@ export async function read_file_range({ filePath, startLine, endLine }) {
|
|
|
185
214
|
debugLog(` - Absolute path starts with '/': ${absolutePath.startsWith('/')}`);
|
|
186
215
|
debugLog(` - Absolute path length: ${absolutePath.length}`);
|
|
187
216
|
|
|
217
|
+
// 파일 크기 제한 검사 (보안)
|
|
218
|
+
debugLog(`Checking file size...`);
|
|
219
|
+
try {
|
|
220
|
+
const stat = await safeStat(absolutePath);
|
|
221
|
+
const fileSizeBytes = stat.size;
|
|
222
|
+
debugLog(`File size: ${fileSizeBytes} bytes (${(fileSizeBytes / 1024 / 1024).toFixed(2)} MB)`);
|
|
223
|
+
|
|
224
|
+
if (fileSizeBytes > MAX_FILE_SIZE_BYTES) {
|
|
225
|
+
debugLog(`ERROR: File exceeds ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB size limit`);
|
|
226
|
+
debugLog('========== read_file_range ERROR END ==========');
|
|
227
|
+
// trim된 파일 목록에서 제거 (재읽기 시도했으므로 알림 불필요)
|
|
228
|
+
markFileAsReRead(absolutePath);
|
|
229
|
+
return {
|
|
230
|
+
operation_successful: false,
|
|
231
|
+
error_message: `File exceeds 10MB size limit (actual: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB). Large files cannot be read for security reasons.`,
|
|
232
|
+
target_file_path: absolutePath,
|
|
233
|
+
file_size_bytes: fileSizeBytes
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
} catch (statError) {
|
|
237
|
+
debugLog(`Stat failed (file may not exist): ${statError.message}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
188
240
|
// 파일 전체를 읽고 범위만 추출
|
|
189
241
|
debugLog(`Reading file...`);
|
|
190
242
|
const content = await safeReadFile(absolutePath, 'utf8');
|
|
@@ -206,6 +258,9 @@ export async function read_file_range({ filePath, startLine, endLine }) {
|
|
|
206
258
|
await trackFileRead(absolutePath, content);
|
|
207
259
|
debugLog(`File read tracked`);
|
|
208
260
|
|
|
261
|
+
// trim된 파일 목록에서 제거 (다시 읽었으므로 알림 불필요)
|
|
262
|
+
markFileAsReRead(absolutePath);
|
|
263
|
+
|
|
209
264
|
// 스냅샷 저장 (UI 미리보기용)
|
|
210
265
|
debugLog(`Saving file snapshot...`);
|
|
211
266
|
saveFileSnapshot(absolutePath, content);
|
|
@@ -263,6 +318,8 @@ export async function read_file_range({ filePath, startLine, endLine }) {
|
|
|
263
318
|
|
|
264
319
|
// 에러 시에도 절대경로로 반환
|
|
265
320
|
const absolutePath = resolve(filePath);
|
|
321
|
+
// trim된 파일 목록에서 제거 (재읽기 시도했으므로 알림 불필요)
|
|
322
|
+
markFileAsReRead(absolutePath);
|
|
266
323
|
return {
|
|
267
324
|
operation_successful: false,
|
|
268
325
|
error_message: error.message,
|
package/src/tools/glob.js
CHANGED
|
@@ -40,9 +40,6 @@ export async function globSearch({
|
|
|
40
40
|
debugLog(` maxResults: ${maxResults}`);
|
|
41
41
|
debugLog(` - Current Working Directory: ${process.cwd()}`);
|
|
42
42
|
|
|
43
|
-
// Intentional delay for testing pending state
|
|
44
|
-
await new Promise(resolve => setTimeout(resolve, 13));
|
|
45
|
-
|
|
46
43
|
try {
|
|
47
44
|
if (typeof pattern !== 'string' || !pattern.trim()) {
|
|
48
45
|
debugLog(`ERROR: Invalid pattern`);
|
package/src/tools/ripgrep.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { theme } from '../frontend/design/themeColors.js';
|
|
3
|
+
import {
|
|
4
|
+
RIPGREP_DEFAULT_TIMEOUT_MS as DEFAULT_TIMEOUT_MS,
|
|
5
|
+
RIPGREP_DEFAULT_MAX_COUNT as DEFAULT_MAX_COUNT,
|
|
6
|
+
RIPGREP_MAX_OUTPUT_SIZE as MAX_OUTPUT_SIZE
|
|
7
|
+
} from '../config/constants.js';
|
|
3
8
|
|
|
4
9
|
// 파일 내용 검색, 파일 경로 필터링, 다양한 출력 모드 등을 지원합니다.
|
|
5
10
|
|
|
6
|
-
const DEFAULT_TIMEOUT_MS = 120000;
|
|
7
|
-
const DEFAULT_MAX_COUNT = 500;
|
|
8
|
-
const MAX_OUTPUT_SIZE = 30000; // 30KB 출력 크기 제한
|
|
9
|
-
|
|
10
11
|
/**
|
|
11
12
|
* ripgrep 인수를 구성합니다
|
|
12
13
|
*/
|
|
@@ -332,9 +333,6 @@ export async function ripgrep({
|
|
|
332
333
|
head_limit: headLimit = null,
|
|
333
334
|
includeHidden = false
|
|
334
335
|
}) {
|
|
335
|
-
// Intentional delay for testing pending state
|
|
336
|
-
await new Promise(resolve => setTimeout(resolve, 13));
|
|
337
|
-
|
|
338
336
|
if (typeof pattern !== 'string' || !pattern.trim()) {
|
|
339
337
|
return {
|
|
340
338
|
operation_successful: false,
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Tool
|
|
3
|
+
* AI Agent가 스킬을 호출할 수 있는 도구
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { discoverAllSkills, loadSkillAsPrompt } from '../system/skill_loader.js';
|
|
7
|
+
import { createDebugLogger } from '../util/debug_log.js';
|
|
8
|
+
|
|
9
|
+
const debugLog = createDebugLogger('skill_tool.log', 'skill_tool');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 스킬 호출 도구 스키마
|
|
13
|
+
*/
|
|
14
|
+
export const skillSchema = {
|
|
15
|
+
type: "function",
|
|
16
|
+
function: {
|
|
17
|
+
name: "invoke_skill",
|
|
18
|
+
description: `스킬을 호출하여 특정 작업을 수행합니다.
|
|
19
|
+
스킬은 특정 작업에 대한 전문화된 지침을 제공합니다.
|
|
20
|
+
사용 가능한 스킬 목록은 시스템 프롬프트의 "Available Skills" 섹션을 참조하세요.
|
|
21
|
+
|
|
22
|
+
사용 시점:
|
|
23
|
+
- 사용자가 슬래시 커맨드(/skill-name)를 요청할 때
|
|
24
|
+
- 특정 작업에 적합한 스킬이 있을 때
|
|
25
|
+
- 코드 리뷰, 설명 등 전문화된 작업이 필요할 때`,
|
|
26
|
+
parameters: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
skill_name: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "호출할 스킬 이름 (예: 'code-review', 'explain')"
|
|
32
|
+
},
|
|
33
|
+
arguments: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "스킬에 전달할 인자 (예: 파일 경로, 설명할 대상 등)"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
required: ["skill_name"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 스킬 호출 함수
|
|
45
|
+
* @param {Object} params - 스킬 호출 파라미터
|
|
46
|
+
* @param {string} params.skill_name - 스킬 이름
|
|
47
|
+
* @param {string} [params.arguments] - 스킬 인자
|
|
48
|
+
* @returns {Promise<Object>} 스킬 실행 결과
|
|
49
|
+
*/
|
|
50
|
+
export async function invokeSkill({ skill_name, arguments: args = '' }) {
|
|
51
|
+
debugLog(`[invokeSkill] Invoking skill: ${skill_name} with args: ${args}`);
|
|
52
|
+
|
|
53
|
+
const result = await loadSkillAsPrompt(skill_name, args);
|
|
54
|
+
|
|
55
|
+
if (!result) {
|
|
56
|
+
debugLog(`[invokeSkill] Skill not found: ${skill_name}`);
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: `Skill not found: ${skill_name}`,
|
|
60
|
+
available_skills: await getAvailableSkillNames()
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
debugLog(`[invokeSkill] Skill loaded successfully: ${skill_name}`);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
skill_name: result.skill.name,
|
|
69
|
+
skill_description: result.skill.description,
|
|
70
|
+
skill_instructions: result.prompt,
|
|
71
|
+
source: result.skill.source
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 사용 가능한 스킬 이름 목록 반환
|
|
77
|
+
* @returns {Promise<string[]>}
|
|
78
|
+
*/
|
|
79
|
+
async function getAvailableSkillNames() {
|
|
80
|
+
const skills = await discoverAllSkills();
|
|
81
|
+
return skills.map(s => s.name);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 시스템 프롬프트에 추가할 스킬 목록 텍스트 생성
|
|
86
|
+
* @returns {Promise<string>}
|
|
87
|
+
*/
|
|
88
|
+
export async function generateSkillListForPrompt() {
|
|
89
|
+
const skills = await discoverAllSkills();
|
|
90
|
+
|
|
91
|
+
if (skills.length === 0) {
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const lines = [
|
|
96
|
+
'',
|
|
97
|
+
'## Available Skills',
|
|
98
|
+
'',
|
|
99
|
+
'다음 스킬들을 사용하여 특정 작업을 수행할 수 있습니다.',
|
|
100
|
+
'스킬을 사용하려면 invoke_skill 도구를 호출하세요.',
|
|
101
|
+
''
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
for (const skill of skills) {
|
|
105
|
+
const desc = skill.description || '(설명 없음)';
|
|
106
|
+
lines.push(`- **${skill.name}**: ${desc}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
lines.push('');
|
|
110
|
+
lines.push('사용자가 슬래시 커맨드(예: /code-review)를 요청하거나,');
|
|
111
|
+
lines.push('위 스킬이 도움이 될 수 있는 작업을 요청하면 해당 스킬을 호출하세요.');
|
|
112
|
+
lines.push('');
|
|
113
|
+
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 스킬 도구 함수 맵
|
|
119
|
+
*/
|
|
120
|
+
export const SKILL_FUNCTIONS = {
|
|
121
|
+
invoke_skill: invokeSkill
|
|
122
|
+
};
|
|
@@ -19,9 +19,6 @@ import { theme } from '../frontend/design/themeColors.js';
|
|
|
19
19
|
* @returns {Promise<{operation_successful: boolean, url: string, content: string, error_message: string|null}>} 결과 객체
|
|
20
20
|
*/
|
|
21
21
|
export async function fetch_web_page({ url }) {
|
|
22
|
-
// Intentional delay for testing pending state
|
|
23
|
-
await new Promise(resolve => setTimeout(resolve, 13));
|
|
24
|
-
|
|
25
22
|
const timeout = 30;
|
|
26
23
|
const user_agent = 'Mozilla/5.0 (compatible; WebFetcher/1.0)';
|
|
27
24
|
const encoding = 'utf-8';
|
|
@@ -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
|
+
}
|