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
@@ -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
  /**
@@ -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
  */
@@ -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
- const logFile = getLogFile();
109
- await safeMkdir(dirname(logFile), { recursive: true }).catch(() => {});
110
- await safeAppendFile(logFile, internalDebugLog.join('\n') + '\n');
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
- const logFile = getLogFile();
134
- await safeMkdir(dirname(logFile), { recursive: true }).catch(() => {});
135
- await safeAppendFile(logFile, internalDebugLog.join('\n') + '\n');
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
- const logFile = getLogFile();
180
- await safeMkdir(dirname(logFile), { recursive: true }).catch(() => {});
181
- await safeAppendFile(logFile, internalDebugLog.join('\n') + '\n').catch(() => {});
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);
@@ -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
- // 2000줄 제한 체크
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`);
@@ -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
+ }