aiexecode 1.0.92 → 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.

Files changed (54) hide show
  1. package/README.md +210 -87
  2. package/index.js +33 -1
  3. package/package.json +3 -3
  4. package/payload_viewer/out/404/index.html +1 -1
  5. package/payload_viewer/out/404.html +1 -1
  6. package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
  7. package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
  8. package/payload_viewer/out/index.html +1 -1
  9. package/payload_viewer/out/index.txt +3 -3
  10. package/payload_viewer/web_server.js +361 -0
  11. package/src/LLMClient/client.js +392 -16
  12. package/src/LLMClient/converters/responses-to-claude.js +67 -18
  13. package/src/LLMClient/converters/responses-to-zai.js +608 -0
  14. package/src/LLMClient/errors.js +18 -4
  15. package/src/LLMClient/index.js +5 -0
  16. package/src/ai_based/completion_judge.js +35 -4
  17. package/src/ai_based/orchestrator.js +146 -35
  18. package/src/commands/agents.js +70 -0
  19. package/src/commands/commands.js +51 -0
  20. package/src/commands/debug.js +52 -0
  21. package/src/commands/help.js +11 -1
  22. package/src/commands/model.js +43 -7
  23. package/src/commands/skills.js +46 -0
  24. package/src/config/ai_models.js +96 -5
  25. package/src/config/constants.js +71 -0
  26. package/src/frontend/App.js +4 -5
  27. package/src/frontend/components/ConversationItem.js +25 -24
  28. package/src/frontend/components/HelpView.js +106 -2
  29. package/src/frontend/components/SetupWizard.js +53 -8
  30. package/src/frontend/utils/syntaxHighlighter.js +4 -4
  31. package/src/frontend/utils/toolUIFormatter.js +261 -0
  32. package/src/system/agents_loader.js +289 -0
  33. package/src/system/ai_request.js +147 -9
  34. package/src/system/command_parser.js +33 -3
  35. package/src/system/conversation_state.js +265 -0
  36. package/src/system/custom_command_loader.js +386 -0
  37. package/src/system/session.js +59 -35
  38. package/src/system/skill_loader.js +318 -0
  39. package/src/system/tool_approval.js +10 -0
  40. package/src/tools/file_reader.js +49 -9
  41. package/src/tools/glob.js +0 -3
  42. package/src/tools/ripgrep.js +5 -7
  43. package/src/tools/skill_tool.js +122 -0
  44. package/src/tools/web_downloader.js +0 -3
  45. package/src/util/clone.js +174 -0
  46. package/src/util/config.js +38 -2
  47. package/src/util/config_migration.js +174 -0
  48. package/src/util/path_validator.js +178 -0
  49. package/src/util/prompt_loader.js +68 -1
  50. package/src/util/safe_fs.js +43 -3
  51. package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
  52. /package/payload_viewer/out/_next/static/{d0-fu2rgYnshgGFPxr1CR → lHmNygVpv4N1VR0LdnwkJ}/_buildManifest.js +0 -0
  53. /package/payload_viewer/out/_next/static/{d0-fu2rgYnshgGFPxr1CR → lHmNygVpv4N1VR0LdnwkJ}/_clientMiddlewareManifest.json +0 -0
  54. /package/payload_viewer/out/_next/static/{d0-fu2rgYnshgGFPxr1CR → lHmNygVpv4N1VR0LdnwkJ}/_ssgManifest.js +0 -0
@@ -9,6 +9,7 @@ import { CODE_EDITOR_FUNCTIONS } from "../tools/code_editor.js";
9
9
  import { WEB_DOWNLOADER_FUNCTIONS } from "../tools/web_downloader.js";
10
10
  import { RESPONSE_MESSAGE_FUNCTIONS } from '../tools/response_message.js';
11
11
  import { TODO_WRITE_FUNCTIONS } from '../tools/todo_write.js';
12
+ import { SKILL_FUNCTIONS } from '../tools/skill_tool.js';
12
13
  import { clampOutput, formatToolStdout } from "../util/output_formatter.js";
13
14
  import { buildToolHistoryEntry } from "../util/rag_helper.js";
14
15
  import { createSessionData, getLastConversationState, loadPreviousSessions, saveSessionToHistory, saveTodosToSession, restoreTodosFromSession, updateCurrentTodos, getCurrentTodos } from "./session_memory.js";
@@ -21,16 +22,14 @@ import { safeReadFile } from '../util/safe_fs.js';
21
22
  import { resolve } from 'path';
22
23
  import { createDebugLogger } from '../util/debug_log.js';
23
24
  import { ERROR_VERBOSITY } from '../config/feature_flags.js';
25
+ import {
26
+ MAX_REASONING_ONLY_RESPONSES,
27
+ DEFAULT_MAX_ITERATIONS,
28
+ SUB_MISSION_MAX_LENGTH
29
+ } from '../config/constants.js';
24
30
 
25
31
  const debugLog = createDebugLogger('session.log', 'session');
26
32
 
27
- /**
28
- * 상수 정의
29
- */
30
- const MAX_REASONING_ONLY_RESPONSES = 5;
31
- const DEFAULT_MAX_ITERATIONS = 50;
32
- const SUB_MISSION_MAX_LENGTH = 120;
33
-
34
33
 
35
34
  /**
36
35
  * 서브미션 이름을 추론하는 헬퍼 함수
@@ -391,6 +390,9 @@ async function processOrchestratorResponses(params) {
391
390
  );
392
391
  debugLog(`Has processable output: ${hasProcessableOutput}`);
393
392
 
393
+ // pendingMessageOutput은 while 루프 스코프에서 유지 (function call 없이 끝날 때 출력용)
394
+ let pendingMessageOutput = null;
395
+
394
396
  if (!hasProcessableOutput) {
395
397
  debugLog(`No processable output, reasoningOnlyResponses: ${reasoningOnlyResponses}`);
396
398
  if (reasoningOnlyResponses >= MAX_REASONING_ONLY_RESPONSES) {
@@ -419,7 +421,6 @@ async function processOrchestratorResponses(params) {
419
421
  reasoningOnlyResponses = 0;
420
422
  let executedFunctionCall = false;
421
423
  let lastOutputType = null;
422
- let pendingMessageOutput = null; // 출력 대기 중인 MESSAGE 저장
423
424
 
424
425
  // 각 출력 처리
425
426
  for (let outputIndex = 0; outputIndex < outputs.length; outputIndex++) {
@@ -598,8 +599,33 @@ async function processOrchestratorResponses(params) {
598
599
 
599
600
  // 완료 조건 체크
600
601
  debugLog(`Checking completion conditions...`);
601
- if (!executedFunctionCall && lastOutputType === 'message') {
602
- debugLog(`COMPLETION: No function call + message type - judging if mission is truly complete`);
602
+ debugLog(` executedFunctionCall: ${executedFunctionCall}, hadAnyFunctionCall: ${hadAnyFunctionCall}, lastOutputType: ${lastOutputType}`);
603
+
604
+ // Case 1: Function call 없이 message만 있는 경우 (단순 대화, 인사 등)
605
+ // → Completion Judge 없이 바로 message 출력하고 완료
606
+ if (!executedFunctionCall && lastOutputType === 'message' && !hadAnyFunctionCall) {
607
+ debugLog(`COMPLETION: No function call at all + message type - completing immediately without Completion Judge`);
608
+
609
+ // 대기 중인 메시지 출력
610
+ if (pendingMessageOutput) {
611
+ debugLog(`Displaying pending message immediately`);
612
+ processMessageOutput(pendingMessageOutput, true);
613
+ }
614
+
615
+ missionSolved = true;
616
+ break;
617
+ }
618
+
619
+ // Case 2: Function call 실행 후 message로 결과 보고
620
+ // → message 먼저 출력 → Completion Judge로 추가 작업 판단
621
+ if (!executedFunctionCall && lastOutputType === 'message' && hadAnyFunctionCall) {
622
+ debugLog(`COMPLETION: Had function calls + final message - displaying message first, then judging completion`);
623
+
624
+ // 먼저 message 출력 (사용자가 바로 결과 확인 가능)
625
+ if (pendingMessageOutput) {
626
+ debugLog(`Displaying pending message before completion judge`);
627
+ processMessageOutput(pendingMessageOutput, true);
628
+ }
603
629
 
604
630
  // 세션 중단 확인 (completion_judge 호출 전)
605
631
  if (sessionInterrupted) {
@@ -646,45 +672,30 @@ async function processOrchestratorResponses(params) {
646
672
  missionSolved = judgement.shouldComplete;
647
673
 
648
674
  if (missionSolved) {
649
- // 미션이 완료되었다고 판단되면 대기 중인 메시지 출력 후 종료
650
- debugLog(`Mission judged as complete, displaying pending message and breaking loop`);
651
- if (pendingMessageOutput) {
652
- processMessageOutput(pendingMessageOutput, true); // 이제 출력
653
- }
675
+ // 미션이 완료되었다고 판단 (message는 이미 출력됨)
676
+ debugLog(`Mission judged as complete, breaking loop`);
654
677
  break;
655
678
  } else {
656
- // 미션이 완료되지 않았다고 판단되면 메시지를 출력하지 않고 계속 진행
657
- debugLog(`Mission not complete, continuing without displaying message`);
658
-
659
- // orchestratorConversation에서 방금 추가된 assistant message에 _internal_only 플래그 추가
660
- const conversation = getOrchestratorConversation();
661
- for (let i = conversation.length - 1; i >= 0; i--) {
662
- const entry = conversation[i];
663
- if (entry.type === 'message' && entry.role === 'assistant') {
664
- entry._internal_only = true;
665
- debugLog(`[_internal_only] Marked assistant message at index ${i} as internal-only (not for display)`);
666
- break;
667
- }
668
- }
679
+ // 미션이 완료되지 않았다고 판단되면 계속 진행
680
+ // (message는 이미 출력됨 - 사용자가 진행 상황 확인 가능)
681
+ debugLog(`Mission not complete, continuing...`);
669
682
 
670
683
  // whatUserShouldSay를 새로운 사용자 요구사항으로 처리
671
684
  if (judgement.whatUserShouldSay && judgement.whatUserShouldSay.trim().length > 0) {
672
685
  debugLog(`Treating whatUserShouldSay as new user request: ${judgement.whatUserShouldSay}`);
673
- // 💬 Auto-continuing 메시지 제거
674
686
 
675
687
  // whatUserShouldSay를 새로운 mission으로 설정하여 루프를 빠져나감
676
- // 상위 runSession 루프에서 다시 orchestrateMission이 호출될 것임
677
688
  improvementPoints = judgement.whatUserShouldSay;
678
- improvementPointsIsAutoGenerated = true; // auto-generated 플래그 설정
689
+ improvementPointsIsAutoGenerated = true;
679
690
  debugLog(`[_internal_only] Set improvementPointsIsAutoGenerated=true for whatUserShouldSay`);
680
- missionSolved = false; // 완료되지 않았으므로 상위 루프 계속
681
- break; // processOrchestratorResponses 루프 종료
691
+ missionSolved = false;
692
+ break;
682
693
  } else {
683
694
  // whatUserShouldSay가 없으면 빈 문자열로 계속 진행
684
695
  improvementPoints = "";
685
696
  debugLog(`Continuing mission without whatUserShouldSay`);
686
697
 
687
- // 세션 중단 확인 (continueOrchestratorConversation 호출 전)
698
+ // 세션 중단 확인
688
699
  if (sessionInterrupted) {
689
700
  debugLog(`Session interrupted before continueOrchestratorConversation, breaking loop`);
690
701
  break;
@@ -693,7 +704,7 @@ async function processOrchestratorResponses(params) {
693
704
  try {
694
705
  orchestratedResponse = await continueOrchestratorConversation();
695
706
  debugLog(`Received response with ${orchestratedResponse?.output?.length || 0} outputs`);
696
- continue; // 루프 처음으로 돌아가서 새 응답 처리
707
+ continue;
697
708
  } catch (err) {
698
709
  if (err.name === 'AbortError' || sessionInterrupted) {
699
710
  debugLog(`Conversation aborted or interrupted: ${err.name}`);
@@ -706,11 +717,23 @@ async function processOrchestratorResponses(params) {
706
717
  }
707
718
  }
708
719
 
720
+ // Case 3: Function call이 없는 기타 경우
709
721
  if (!executedFunctionCall) {
710
722
  debugLog(`COMPLETION: No function call executed (other cases), breaking loop`);
723
+ if (pendingMessageOutput) {
724
+ debugLog(`Displaying pending message before completion`);
725
+ processMessageOutput(pendingMessageOutput, true);
726
+ }
711
727
  break;
712
728
  }
713
729
 
730
+ // Function call 실행 후 계속 진행하기 전에 pending message가 있으면 출력
731
+ // (function_call과 message가 같이 온 경우)
732
+ if (pendingMessageOutput) {
733
+ debugLog(`Displaying pending message before continuing (function_call + message case)`);
734
+ processMessageOutput(pendingMessageOutput, true);
735
+ }
736
+
714
737
  // continueOrchestratorConversation 호출
715
738
  debugLog(`Continuing orchestrator conversation for next iteration...`);
716
739
  try {
@@ -899,6 +922,7 @@ export async function runSession(options) {
899
922
  ...GLOB_FUNCTIONS,
900
923
  ...RESPONSE_MESSAGE_FUNCTIONS,
901
924
  ...TODO_WRITE_FUNCTIONS,
925
+ ...SKILL_FUNCTIONS, // 스킬 호출 도구
902
926
  ...mcpToolFunctions,
903
927
  "bash": async (args) => execShellScript(args.script)
904
928
  };
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Skill Loader
3
+ *
4
+ * Claude Code 스타일의 스킬 시스템 구현
5
+ * 스킬은 SKILL.md 파일이 포함된 폴더로, AI에게 특정 작업 수행 방법을 가르칩니다.
6
+ *
7
+ * 스킬 위치 (우선순위 순):
8
+ * 1. Project: CWD/.aiexe/skills/<skill-name>/SKILL.md
9
+ * 2. Personal: ~/.aiexe/skills/<skill-name>/SKILL.md
10
+ */
11
+
12
+ import { join, basename, dirname } from 'path';
13
+ import { safeReadFile, safeReaddir, safeStat, safeAccess } from '../util/safe_fs.js';
14
+ import { PERSONAL_SKILLS_DIR, PROJECT_SKILLS_DIR } from '../util/config.js';
15
+ import { createDebugLogger } from '../util/debug_log.js';
16
+
17
+ const debugLog = createDebugLogger('skill_loader.log', 'skill_loader');
18
+
19
+ /**
20
+ * 스킬 정보 객체
21
+ * @typedef {Object} Skill
22
+ * @property {string} name - 스킬 이름 (폴더명 또는 frontmatter의 name)
23
+ * @property {string} description - 스킬 설명
24
+ * @property {string} path - SKILL.md 파일 경로
25
+ * @property {string} directory - 스킬 폴더 경로
26
+ * @property {string} source - 'project' | 'personal'
27
+ * @property {Object} frontmatter - YAML frontmatter 파싱 결과
28
+ * @property {string} content - 마크다운 내용 (frontmatter 제외)
29
+ */
30
+
31
+ /**
32
+ * YAML frontmatter를 파싱합니다.
33
+ * @param {string} content - SKILL.md 파일 내용
34
+ * @returns {{ frontmatter: Object, content: string }}
35
+ */
36
+ function parseFrontmatter(content) {
37
+ const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
38
+ const match = content.match(frontmatterRegex);
39
+
40
+ if (!match) {
41
+ return {
42
+ frontmatter: {},
43
+ content: content.trim()
44
+ };
45
+ }
46
+
47
+ const yamlContent = match[1];
48
+ const markdownContent = match[2];
49
+
50
+ // 간단한 YAML 파싱 (key: value 형식만 지원)
51
+ const frontmatter = {};
52
+ const lines = yamlContent.split('\n');
53
+
54
+ for (const line of lines) {
55
+ const colonIndex = line.indexOf(':');
56
+ if (colonIndex > 0) {
57
+ const key = line.substring(0, colonIndex).trim();
58
+ let value = line.substring(colonIndex + 1).trim();
59
+
60
+ // 따옴표 제거
61
+ if ((value.startsWith('"') && value.endsWith('"')) ||
62
+ (value.startsWith("'") && value.endsWith("'"))) {
63
+ value = value.slice(1, -1);
64
+ }
65
+
66
+ // boolean 변환
67
+ if (value === 'true') value = true;
68
+ else if (value === 'false') value = false;
69
+
70
+ frontmatter[key] = value;
71
+ }
72
+ }
73
+
74
+ return {
75
+ frontmatter,
76
+ content: markdownContent.trim()
77
+ };
78
+ }
79
+
80
+ /**
81
+ * 단일 스킬 폴더를 로드합니다.
82
+ * @param {string} skillDir - 스킬 폴더 경로
83
+ * @param {string} source - 'project' | 'personal'
84
+ * @returns {Promise<Skill|null>}
85
+ */
86
+ async function loadSkillFromDirectory(skillDir, source) {
87
+ const skillMdPath = join(skillDir, 'SKILL.md');
88
+
89
+ try {
90
+ await safeAccess(skillMdPath);
91
+ } catch {
92
+ debugLog(`[loadSkillFromDirectory] No SKILL.md found in ${skillDir}`);
93
+ return null;
94
+ }
95
+
96
+ try {
97
+ const content = await safeReadFile(skillMdPath, 'utf8');
98
+ const { frontmatter, content: markdownContent } = parseFrontmatter(content);
99
+
100
+ const folderName = basename(skillDir);
101
+ const name = frontmatter.name || folderName;
102
+
103
+ // description이 없으면 마크다운 첫 단락 사용
104
+ let description = frontmatter.description;
105
+ if (!description && markdownContent) {
106
+ const firstParagraph = markdownContent.split('\n\n')[0];
107
+ description = firstParagraph.replace(/^#.*\n?/, '').trim().substring(0, 200);
108
+ }
109
+
110
+ const skill = {
111
+ name,
112
+ description: description || '',
113
+ path: skillMdPath,
114
+ directory: skillDir,
115
+ source,
116
+ frontmatter,
117
+ content: markdownContent
118
+ };
119
+
120
+ debugLog(`[loadSkillFromDirectory] Loaded skill: ${name} from ${source}`);
121
+ return skill;
122
+ } catch (error) {
123
+ debugLog(`[loadSkillFromDirectory] Error loading skill from ${skillDir}: ${error.message}`);
124
+ return null;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * 지정된 디렉토리에서 모든 스킬을 검색합니다.
130
+ * @param {string} skillsDir - 스킬 폴더 경로
131
+ * @param {string} source - 'project' | 'personal'
132
+ * @returns {Promise<Skill[]>}
133
+ */
134
+ async function discoverSkillsInDirectory(skillsDir, source) {
135
+ const skills = [];
136
+
137
+ try {
138
+ await safeAccess(skillsDir);
139
+ } catch {
140
+ debugLog(`[discoverSkillsInDirectory] Skills directory not found: ${skillsDir}`);
141
+ return skills;
142
+ }
143
+
144
+ try {
145
+ const entries = await safeReaddir(skillsDir);
146
+
147
+ for (const entry of entries) {
148
+ const entryPath = join(skillsDir, entry);
149
+ const stat = await safeStat(entryPath);
150
+
151
+ if (stat.isDirectory()) {
152
+ const skill = await loadSkillFromDirectory(entryPath, source);
153
+ if (skill) {
154
+ skills.push(skill);
155
+ }
156
+ }
157
+ }
158
+ } catch (error) {
159
+ debugLog(`[discoverSkillsInDirectory] Error scanning ${skillsDir}: ${error.message}`);
160
+ }
161
+
162
+ return skills;
163
+ }
164
+
165
+ /**
166
+ * 모든 스킬을 검색합니다.
167
+ * Project 스킬이 Personal 스킬보다 우선순위가 높습니다.
168
+ * @returns {Promise<Skill[]>}
169
+ */
170
+ export async function discoverAllSkills() {
171
+ debugLog('[discoverAllSkills] Starting skill discovery...');
172
+
173
+ const allSkills = new Map(); // name -> skill (중복 방지, 우선순위 적용)
174
+
175
+ // 1. Personal skills (낮은 우선순위)
176
+ const personalSkills = await discoverSkillsInDirectory(PERSONAL_SKILLS_DIR, 'personal');
177
+ for (const skill of personalSkills) {
178
+ allSkills.set(skill.name, skill);
179
+ }
180
+ debugLog(`[discoverAllSkills] Found ${personalSkills.length} personal skills`);
181
+
182
+ // 2. Project skills (높은 우선순위 - 덮어씀)
183
+ const projectSkillsDir = join(process.cwd(), PROJECT_SKILLS_DIR);
184
+ const projectSkills = await discoverSkillsInDirectory(projectSkillsDir, 'project');
185
+ for (const skill of projectSkills) {
186
+ allSkills.set(skill.name, skill);
187
+ }
188
+ debugLog(`[discoverAllSkills] Found ${projectSkills.length} project skills`);
189
+
190
+ const skills = Array.from(allSkills.values());
191
+ debugLog(`[discoverAllSkills] Total unique skills: ${skills.length}`);
192
+
193
+ return skills;
194
+ }
195
+
196
+ /**
197
+ * 이름으로 스킬을 찾습니다.
198
+ * @param {string} skillName - 스킬 이름
199
+ * @returns {Promise<Skill|null>}
200
+ */
201
+ export async function findSkillByName(skillName) {
202
+ debugLog(`[findSkillByName] Looking for skill: ${skillName}`);
203
+
204
+ // Project 스킬 먼저 확인 (높은 우선순위)
205
+ const projectSkillDir = join(process.cwd(), PROJECT_SKILLS_DIR, skillName);
206
+ const projectSkill = await loadSkillFromDirectory(projectSkillDir, 'project');
207
+ if (projectSkill) {
208
+ debugLog(`[findSkillByName] Found project skill: ${skillName}`);
209
+ return projectSkill;
210
+ }
211
+
212
+ // Personal 스킬 확인
213
+ const personalSkillDir = join(PERSONAL_SKILLS_DIR, skillName);
214
+ const personalSkill = await loadSkillFromDirectory(personalSkillDir, 'personal');
215
+ if (personalSkill) {
216
+ debugLog(`[findSkillByName] Found personal skill: ${skillName}`);
217
+ return personalSkill;
218
+ }
219
+
220
+ debugLog(`[findSkillByName] Skill not found: ${skillName}`);
221
+ return null;
222
+ }
223
+
224
+ /**
225
+ * 스킬 내용에서 $ARGUMENTS를 실제 인자로 치환합니다.
226
+ * @param {string} content - 스킬 마크다운 내용
227
+ * @param {string} args - 사용자가 전달한 인자
228
+ * @returns {string}
229
+ */
230
+ export function substituteArguments(content, args) {
231
+ if (!args) return content;
232
+
233
+ // $ARGUMENTS 치환
234
+ let result = content.replace(/\$ARGUMENTS/g, args);
235
+
236
+ // $ARGUMENTS가 없었으면 끝에 추가
237
+ if (result === content && args.trim()) {
238
+ result = content + '\n\nARGUMENTS: ' + args;
239
+ }
240
+
241
+ return result;
242
+ }
243
+
244
+ /**
245
+ * 스킬을 로드하고 프롬프트로 변환합니다.
246
+ * @param {string} skillName - 스킬 이름
247
+ * @param {string} [args] - 스킬에 전달할 인자
248
+ * @returns {Promise<{ prompt: string, skill: Skill } | null>}
249
+ */
250
+ export async function loadSkillAsPrompt(skillName, args = '') {
251
+ const skill = await findSkillByName(skillName);
252
+ if (!skill) {
253
+ return null;
254
+ }
255
+
256
+ let prompt = skill.content;
257
+
258
+ // 인자 치환
259
+ prompt = substituteArguments(prompt, args);
260
+
261
+ // 참조 파일 로드 (선택적)
262
+ // TODO: reference.md, examples.md 등 자동 로드
263
+
264
+ debugLog(`[loadSkillAsPrompt] Loaded skill '${skillName}' with ${args ? 'args' : 'no args'}`);
265
+
266
+ return {
267
+ prompt,
268
+ skill
269
+ };
270
+ }
271
+
272
+ /**
273
+ * 스킬 목록을 포맷팅하여 반환합니다.
274
+ * @returns {Promise<string>}
275
+ */
276
+ export async function formatSkillList() {
277
+ const skills = await discoverAllSkills();
278
+
279
+ if (skills.length === 0) {
280
+ return [
281
+ 'No skills found.',
282
+ '',
283
+ 'Skill locations:',
284
+ ` Personal: ~/.aiexe/skills/<skill-name>/SKILL.md`,
285
+ ` Project: .aiexe/skills/<skill-name>/SKILL.md`,
286
+ '',
287
+ 'Create a skill by making a folder with a SKILL.md file.'
288
+ ].join('\n');
289
+ }
290
+
291
+ const lines = ['Available Skills:', ''];
292
+
293
+ // 소스별 그룹화
294
+ const projectSkills = skills.filter(s => s.source === 'project');
295
+ const personalSkills = skills.filter(s => s.source === 'personal');
296
+
297
+ if (projectSkills.length > 0) {
298
+ lines.push('Project Skills (.aiexe/skills/):');
299
+ for (const skill of projectSkills) {
300
+ const desc = skill.description ? ` - ${skill.description.substring(0, 60)}` : '';
301
+ lines.push(` /${skill.name}${desc}`);
302
+ }
303
+ lines.push('');
304
+ }
305
+
306
+ if (personalSkills.length > 0) {
307
+ lines.push('Personal Skills (~/.aiexe/skills/):');
308
+ for (const skill of personalSkills) {
309
+ const desc = skill.description ? ` - ${skill.description.substring(0, 60)}` : '';
310
+ lines.push(` /${skill.name}${desc}`);
311
+ }
312
+ lines.push('');
313
+ }
314
+
315
+ lines.push('Usage: /<skill-name> [arguments]');
316
+
317
+ return lines.join('\n');
318
+ }
@@ -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;
@@ -1,10 +1,14 @@
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
5
  import { createDebugLogger } from '../util/debug_log.js';
6
6
  import { toDisplayPath } from '../util/path_helper.js';
7
7
  import { theme } from '../frontend/design/themeColors.js';
8
+ import {
9
+ FILE_READER_MAX_LINES as MAX_LINES,
10
+ FILE_READER_MAX_SIZE_BYTES as MAX_FILE_SIZE_BYTES
11
+ } from '../config/constants.js';
8
12
 
9
13
  const debugLog = createDebugLogger('file_reader.log', 'file_reader');
10
14
 
@@ -32,9 +36,6 @@ export async function read_file({ filePath }) {
32
36
  debugLog(` - filePath starts with '../': ${filePath?.startsWith('../') || false}`);
33
37
  debugLog(` - Current Working Directory: ${process.cwd()}`);
34
38
 
35
- // Intentional delay for testing pending state
36
- await new Promise(resolve => setTimeout(resolve, 13));
37
-
38
39
  try {
39
40
  // 경로를 절대경로로 정규화
40
41
  const absolutePath = resolve(filePath);
@@ -45,6 +46,28 @@ export async function read_file({ filePath }) {
45
46
  debugLog(` - Absolute path starts with '/': ${absolutePath.startsWith('/')}`);
46
47
  debugLog(` - Absolute path length: ${absolutePath.length}`);
47
48
 
49
+ // 파일 크기 제한 검사 (보안)
50
+ debugLog(`Checking file size...`);
51
+ try {
52
+ const stat = await safeStat(absolutePath);
53
+ const fileSizeBytes = stat.size;
54
+ debugLog(`File size: ${fileSizeBytes} bytes (${(fileSizeBytes / 1024 / 1024).toFixed(2)} MB)`);
55
+
56
+ if (fileSizeBytes > MAX_FILE_SIZE_BYTES) {
57
+ debugLog(`ERROR: File exceeds ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB size limit`);
58
+ debugLog('========== read_file ERROR END ==========');
59
+ return {
60
+ operation_successful: false,
61
+ error_message: `File exceeds 10MB size limit (actual: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB). Large files cannot be read for security reasons.`,
62
+ target_file_path: absolutePath,
63
+ file_size_bytes: fileSizeBytes
64
+ };
65
+ }
66
+ } catch (statError) {
67
+ // stat 실패 시 파일이 존재하지 않을 수 있음 - 읽기에서 처리
68
+ debugLog(`Stat failed (file may not exist): ${statError.message}`);
69
+ }
70
+
48
71
  debugLog(`Reading file...`);
49
72
  const content = await safeReadFile(absolutePath, 'utf8');
50
73
  debugLog(`File read successful: ${content.length} bytes`);
@@ -56,8 +79,7 @@ export async function read_file({ filePath }) {
56
79
  (content.endsWith('\n') ? lines.length - 1 : lines.length);
57
80
  debugLog(`Total lines: ${totalLines}`);
58
81
 
59
- // 2000줄 제한 체크
60
- const MAX_LINES = 2000;
82
+ // 줄 제한 체크
61
83
  if (totalLines > MAX_LINES) {
62
84
  debugLog(`ERROR: File exceeds ${MAX_LINES} lines limit`);
63
85
  debugLog('========== read_file ERROR END ==========');
@@ -172,9 +194,6 @@ export async function read_file_range({ filePath, startLine, endLine }) {
172
194
  debugLog(` endLine: ${endLine}`);
173
195
  debugLog(` - Current Working Directory: ${process.cwd()}`);
174
196
 
175
- // Intentional delay for testing pending state
176
- await new Promise(resolve => setTimeout(resolve, 13));
177
-
178
197
  try {
179
198
  // 경로를 절대경로로 정규화
180
199
  const absolutePath = resolve(filePath);
@@ -185,6 +204,27 @@ export async function read_file_range({ filePath, startLine, endLine }) {
185
204
  debugLog(` - Absolute path starts with '/': ${absolutePath.startsWith('/')}`);
186
205
  debugLog(` - Absolute path length: ${absolutePath.length}`);
187
206
 
207
+ // 파일 크기 제한 검사 (보안)
208
+ debugLog(`Checking file size...`);
209
+ try {
210
+ const stat = await safeStat(absolutePath);
211
+ const fileSizeBytes = stat.size;
212
+ debugLog(`File size: ${fileSizeBytes} bytes (${(fileSizeBytes / 1024 / 1024).toFixed(2)} MB)`);
213
+
214
+ if (fileSizeBytes > MAX_FILE_SIZE_BYTES) {
215
+ debugLog(`ERROR: File exceeds ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB size limit`);
216
+ debugLog('========== read_file_range ERROR END ==========');
217
+ return {
218
+ operation_successful: false,
219
+ error_message: `File exceeds 10MB size limit (actual: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB). Large files cannot be read for security reasons.`,
220
+ target_file_path: absolutePath,
221
+ file_size_bytes: fileSizeBytes
222
+ };
223
+ }
224
+ } catch (statError) {
225
+ debugLog(`Stat failed (file may not exist): ${statError.message}`);
226
+ }
227
+
188
228
  // 파일 전체를 읽고 범위만 추출
189
229
  debugLog(`Reading file...`);
190
230
  const content = await safeReadFile(absolutePath, 'utf8');
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,