aiexecode 1.0.94 → 1.0.98
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 +198 -88
- package/index.js +43 -9
- 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/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 +30 -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/apikey.js +1 -1
- 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/frontend/App.js +8 -0
- package/src/frontend/components/AutocompleteMenu.js +7 -1
- 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 +175 -12
- 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/file_reference_parser.js +132 -0
- package/src/util/path_validator.js +178 -0
- package/src/util/prompt_loader.js +68 -1
- package/src/util/safe_fs.js +43 -3
- package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_ssgManifest.js +0 -0
|
@@ -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,
|
|
@@ -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';
|