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.
- 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/App.js +4 -5
- package/src/frontend/components/ConversationItem.js +25 -24
- package/src/frontend/components/HelpView.js +106 -2
- package/src/frontend/components/SetupWizard.js +53 -8
- package/src/frontend/utils/syntaxHighlighter.js +4 -4
- 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/{d0-fu2rgYnshgGFPxr1CR → lHmNygVpv4N1VR0LdnwkJ}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{d0-fu2rgYnshgGFPxr1CR → lHmNygVpv4N1VR0LdnwkJ}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{d0-fu2rgYnshgGFPxr1CR → lHmNygVpv4N1VR0LdnwkJ}/_ssgManifest.js +0 -0
package/src/system/session.js
CHANGED
|
@@ -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
|
-
|
|
602
|
-
|
|
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,
|
|
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
|
-
|
|
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;
|
|
689
|
+
improvementPointsIsAutoGenerated = true;
|
|
679
690
|
debugLog(`[_internal_only] Set improvementPointsIsAutoGenerated=true for whatUserShouldSay`);
|
|
680
|
-
missionSolved = false;
|
|
681
|
-
break;
|
|
691
|
+
missionSolved = false;
|
|
692
|
+
break;
|
|
682
693
|
} else {
|
|
683
694
|
// whatUserShouldSay가 없으면 빈 문자열로 계속 진행
|
|
684
695
|
improvementPoints = "";
|
|
685
696
|
debugLog(`Continuing mission without whatUserShouldSay`);
|
|
686
697
|
|
|
687
|
-
// 세션 중단 확인
|
|
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;
|
package/src/tools/file_reader.js
CHANGED
|
@@ -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
|
-
//
|
|
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`);
|
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,
|