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
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Conversation State Manager
3
+ * orchestratorConversation 전역 변수를 클래스로 캡슐화하여 상태 관리를 개선합니다.
4
+ */
5
+
6
+ import { deepClone } from '../util/clone.js';
7
+
8
+ /**
9
+ * 대화 상태 관리 클래스
10
+ * 전역 대화 상태를 캡슐화하고 안전한 접근 메서드를 제공합니다.
11
+ */
12
+ class ConversationStateManager {
13
+ constructor() {
14
+ /** @type {Array} 대화 히스토리 */
15
+ this._conversation = [];
16
+
17
+ /** @type {Object|null} 요청 옵션 */
18
+ this._requestOptions = null;
19
+
20
+ /** @type {number} 마지막 수정 타임스탬프 */
21
+ this._lastModified = Date.now();
22
+
23
+ /** @type {string|null} 현재 세션 ID */
24
+ this._sessionId = null;
25
+ }
26
+
27
+ /**
28
+ * 대화 히스토리를 가져옵니다.
29
+ * @param {boolean} clone - true이면 깊은 복사본 반환 (기본: false)
30
+ * @returns {Array} 대화 히스토리
31
+ */
32
+ getConversation(clone = false) {
33
+ if (clone) {
34
+ return deepClone(this._conversation);
35
+ }
36
+ return this._conversation;
37
+ }
38
+
39
+ /**
40
+ * 대화 히스토리를 설정합니다.
41
+ * @param {Array} conversation - 새 대화 히스토리
42
+ */
43
+ setConversation(conversation) {
44
+ if (!Array.isArray(conversation)) {
45
+ throw new Error('Conversation must be an array');
46
+ }
47
+ this._conversation = conversation;
48
+ this._lastModified = Date.now();
49
+ }
50
+
51
+ /**
52
+ * 대화에 메시지를 추가합니다.
53
+ * @param {Object} message - 추가할 메시지
54
+ */
55
+ addMessage(message) {
56
+ if (!message || typeof message !== 'object') {
57
+ throw new Error('Message must be an object');
58
+ }
59
+ this._conversation.push(message);
60
+ this._lastModified = Date.now();
61
+ }
62
+
63
+ /**
64
+ * 대화에 여러 메시지를 추가합니다.
65
+ * @param {Array} messages - 추가할 메시지 배열
66
+ */
67
+ addMessages(messages) {
68
+ if (!Array.isArray(messages)) {
69
+ throw new Error('Messages must be an array');
70
+ }
71
+ this._conversation.push(...messages);
72
+ this._lastModified = Date.now();
73
+ }
74
+
75
+ /**
76
+ * 대화 히스토리를 초기화합니다.
77
+ */
78
+ reset() {
79
+ this._conversation = [];
80
+ this._requestOptions = null;
81
+ this._lastModified = Date.now();
82
+ }
83
+
84
+ /**
85
+ * 대화 히스토리를 복원합니다.
86
+ * @param {Array} conversation - 복원할 대화 히스토리
87
+ * @param {Object} requestOptions - 복원할 요청 옵션
88
+ */
89
+ restore(conversation, requestOptions = null) {
90
+ this._conversation = Array.isArray(conversation) ? conversation : [];
91
+ this._requestOptions = requestOptions;
92
+ this._lastModified = Date.now();
93
+ }
94
+
95
+ /**
96
+ * 요청 옵션을 가져옵니다.
97
+ * @returns {Object|null} 요청 옵션
98
+ */
99
+ getRequestOptions() {
100
+ return this._requestOptions;
101
+ }
102
+
103
+ /**
104
+ * 요청 옵션을 설정합니다.
105
+ * @param {Object} options - 요청 옵션
106
+ */
107
+ setRequestOptions(options) {
108
+ this._requestOptions = options;
109
+ this._lastModified = Date.now();
110
+ }
111
+
112
+ /**
113
+ * 대화 히스토리의 길이를 반환합니다.
114
+ * @returns {number} 메시지 수
115
+ */
116
+ get length() {
117
+ return this._conversation.length;
118
+ }
119
+
120
+ /**
121
+ * 대화가 비어있는지 확인합니다.
122
+ * @returns {boolean} 비어있으면 true
123
+ */
124
+ isEmpty() {
125
+ return this._conversation.length === 0;
126
+ }
127
+
128
+ /**
129
+ * 마지막 수정 시간을 반환합니다.
130
+ * @returns {number} 타임스탬프
131
+ */
132
+ getLastModified() {
133
+ return this._lastModified;
134
+ }
135
+
136
+ /**
137
+ * 세션 ID를 설정합니다.
138
+ * @param {string} sessionId - 세션 ID
139
+ */
140
+ setSessionId(sessionId) {
141
+ this._sessionId = sessionId;
142
+ }
143
+
144
+ /**
145
+ * 세션 ID를 가져옵니다.
146
+ * @returns {string|null} 세션 ID
147
+ */
148
+ getSessionId() {
149
+ return this._sessionId;
150
+ }
151
+
152
+ /**
153
+ * 대화에서 특정 조건을 만족하는 메시지를 찾습니다.
154
+ * @param {Function} predicate - 검색 조건 함수
155
+ * @returns {Object|undefined} 찾은 메시지 또는 undefined
156
+ */
157
+ findMessage(predicate) {
158
+ return this._conversation.find(predicate);
159
+ }
160
+
161
+ /**
162
+ * 대화에서 특정 조건을 만족하는 메시지들을 필터링합니다.
163
+ * @param {Function} predicate - 필터 조건 함수
164
+ * @returns {Array} 필터링된 메시지 배열
165
+ */
166
+ filterMessages(predicate) {
167
+ return this._conversation.filter(predicate);
168
+ }
169
+
170
+ /**
171
+ * 마지막 메시지를 가져옵니다.
172
+ * @returns {Object|undefined} 마지막 메시지 또는 undefined
173
+ */
174
+ getLastMessage() {
175
+ if (this._conversation.length === 0) {
176
+ return undefined;
177
+ }
178
+ return this._conversation[this._conversation.length - 1];
179
+ }
180
+
181
+ /**
182
+ * 마지막 N개의 메시지를 가져옵니다.
183
+ * @param {number} count - 가져올 메시지 수
184
+ * @returns {Array} 메시지 배열
185
+ */
186
+ getLastMessages(count) {
187
+ if (count <= 0) {
188
+ return [];
189
+ }
190
+ return this._conversation.slice(-count);
191
+ }
192
+
193
+ /**
194
+ * 대화 히스토리를 트리밍합니다 (오래된 메시지 제거).
195
+ * @param {number} keepCount - 유지할 메시지 수 (시스템 메시지 제외)
196
+ * @returns {number} 제거된 메시지 수
197
+ */
198
+ trim(keepCount) {
199
+ const originalLength = this._conversation.length;
200
+
201
+ // 시스템 메시지는 유지
202
+ const systemMessages = this._conversation.filter(m => m.role === 'system');
203
+ const otherMessages = this._conversation.filter(m => m.role !== 'system');
204
+
205
+ // 최근 keepCount개만 유지
206
+ const keptMessages = otherMessages.slice(-keepCount);
207
+
208
+ this._conversation = [...systemMessages, ...keptMessages];
209
+ this._lastModified = Date.now();
210
+
211
+ return originalLength - this._conversation.length;
212
+ }
213
+
214
+ /**
215
+ * 상태를 직렬화 가능한 객체로 변환합니다.
216
+ * @returns {Object} 직렬화 가능한 상태 객체
217
+ */
218
+ toJSON() {
219
+ return {
220
+ conversation: deepClone(this._conversation),
221
+ requestOptions: this._requestOptions ? deepClone(this._requestOptions) : null,
222
+ lastModified: this._lastModified,
223
+ sessionId: this._sessionId
224
+ };
225
+ }
226
+
227
+ /**
228
+ * 직렬화된 상태에서 복원합니다.
229
+ * @param {Object} state - 직렬화된 상태 객체
230
+ */
231
+ fromJSON(state) {
232
+ if (state.conversation) {
233
+ this._conversation = state.conversation;
234
+ }
235
+ if (state.requestOptions !== undefined) {
236
+ this._requestOptions = state.requestOptions;
237
+ }
238
+ if (state.lastModified) {
239
+ this._lastModified = state.lastModified;
240
+ }
241
+ if (state.sessionId) {
242
+ this._sessionId = state.sessionId;
243
+ }
244
+ }
245
+ }
246
+
247
+ // 싱글톤 인스턴스 생성 및 export
248
+ export const conversationState = new ConversationStateManager();
249
+
250
+ // 후방 호환성을 위한 개별 함수들 export
251
+ export function getOrchestratorConversation() {
252
+ return conversationState.getConversation();
253
+ }
254
+
255
+ export function setOrchestratorConversation(conversation) {
256
+ conversationState.setConversation(conversation);
257
+ }
258
+
259
+ export function resetOrchestratorConversation() {
260
+ conversationState.reset();
261
+ }
262
+
263
+ export function restoreOrchestratorConversation(conversation, requestOptions) {
264
+ conversationState.restore(conversation, requestOptions);
265
+ }
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Custom Command Loader
3
+ *
4
+ * Claude Code 스타일의 Custom Commands 시스템 구현
5
+ * 단일 마크다운 파일로 커맨드를 정의합니다.
6
+ *
7
+ * 커맨드 위치 (우선순위 순):
8
+ * 1. Project: CWD/.aiexe/commands/<command-name>.md
9
+ * 2. Personal: ~/.aiexe/commands/<command-name>.md
10
+ *
11
+ * 사용 예:
12
+ * ~/.aiexe/commands/review.md → /review 커맨드
13
+ * .aiexe/commands/deploy.md → /deploy 커맨드
14
+ */
15
+
16
+ import { join, basename } from 'path';
17
+ import { safeReadFile, safeReaddir, safeStat, safeAccess } from '../util/safe_fs.js';
18
+ import { PERSONAL_COMMANDS_DIR, PROJECT_COMMANDS_DIR } from '../util/config.js';
19
+ import { createDebugLogger } from '../util/debug_log.js';
20
+
21
+ const debugLog = createDebugLogger('custom_command_loader.log', 'custom_command');
22
+
23
+ /**
24
+ * 커맨드 정보 객체
25
+ * @typedef {Object} CustomCommand
26
+ * @property {string} name - 커맨드 이름 (파일명 또는 frontmatter의 name)
27
+ * @property {string} description - 커맨드 설명
28
+ * @property {string} path - 마크다운 파일 경로
29
+ * @property {string} source - 'project' | 'personal'
30
+ * @property {Object} frontmatter - YAML frontmatter 파싱 결과
31
+ * @property {string} content - 마크다운 내용 (frontmatter 제외)
32
+ * @property {string} [argumentHint] - 인자 힌트 (예: "[filename]")
33
+ * @property {boolean} disableModelInvocation - AI 자동 호출 비활성화 여부
34
+ */
35
+
36
+ /**
37
+ * YAML frontmatter를 파싱합니다.
38
+ * @param {string} content - 마크다운 파일 내용
39
+ * @returns {{ frontmatter: Object, content: string }}
40
+ */
41
+ function parseFrontmatter(content) {
42
+ const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
43
+ const match = content.match(frontmatterRegex);
44
+
45
+ if (!match) {
46
+ return {
47
+ frontmatter: {},
48
+ content: content.trim()
49
+ };
50
+ }
51
+
52
+ const yamlContent = match[1];
53
+ const markdownContent = match[2];
54
+
55
+ // 간단한 YAML 파싱 (key: value 형식만 지원)
56
+ const frontmatter = {};
57
+ const lines = yamlContent.split('\n');
58
+
59
+ for (const line of lines) {
60
+ const colonIndex = line.indexOf(':');
61
+ if (colonIndex > 0) {
62
+ const key = line.substring(0, colonIndex).trim();
63
+ let value = line.substring(colonIndex + 1).trim();
64
+
65
+ // 따옴표 제거
66
+ if ((value.startsWith('"') && value.endsWith('"')) ||
67
+ (value.startsWith("'") && value.endsWith("'"))) {
68
+ value = value.slice(1, -1);
69
+ }
70
+
71
+ // boolean 변환
72
+ if (value === 'true') value = true;
73
+ else if (value === 'false') value = false;
74
+
75
+ frontmatter[key] = value;
76
+ }
77
+ }
78
+
79
+ return {
80
+ frontmatter,
81
+ content: markdownContent.trim()
82
+ };
83
+ }
84
+
85
+ /**
86
+ * 단일 커맨드 파일을 로드합니다.
87
+ * @param {string} filePath - 마크다운 파일 경로
88
+ * @param {string} source - 'project' | 'personal'
89
+ * @returns {Promise<CustomCommand|null>}
90
+ */
91
+ async function loadCommandFromFile(filePath, source) {
92
+ try {
93
+ const content = await safeReadFile(filePath, 'utf8');
94
+ const { frontmatter, content: markdownContent } = parseFrontmatter(content);
95
+
96
+ // 파일명에서 .md 제거하여 커맨드 이름 추출
97
+ const fileName = basename(filePath);
98
+ const nameFromFile = fileName.replace(/\.md$/i, '');
99
+ const name = frontmatter.name || nameFromFile;
100
+
101
+ // description이 없으면 마크다운 첫 단락 사용
102
+ let description = frontmatter.description;
103
+ if (!description && markdownContent) {
104
+ const firstParagraph = markdownContent.split('\n\n')[0];
105
+ description = firstParagraph.replace(/^#.*\n?/, '').trim().substring(0, 200);
106
+ }
107
+
108
+ const command = {
109
+ name,
110
+ description: description || '',
111
+ path: filePath,
112
+ source,
113
+ frontmatter,
114
+ content: markdownContent,
115
+ argumentHint: frontmatter['argument-hint'] || frontmatter.argumentHint || null,
116
+ disableModelInvocation: frontmatter['disable-model-invocation'] === true
117
+ };
118
+
119
+ debugLog(`[loadCommandFromFile] Loaded command: ${name} from ${source}`);
120
+ return command;
121
+ } catch (error) {
122
+ debugLog(`[loadCommandFromFile] Error loading command from ${filePath}: ${error.message}`);
123
+ return null;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * 지정된 디렉토리에서 모든 커맨드를 검색합니다.
129
+ * @param {string} commandsDir - 커맨드 폴더 경로
130
+ * @param {string} source - 'project' | 'personal'
131
+ * @returns {Promise<CustomCommand[]>}
132
+ */
133
+ async function discoverCommandsInDirectory(commandsDir, source) {
134
+ const commands = [];
135
+
136
+ try {
137
+ await safeAccess(commandsDir);
138
+ } catch {
139
+ debugLog(`[discoverCommandsInDirectory] Commands directory not found: ${commandsDir}`);
140
+ return commands;
141
+ }
142
+
143
+ try {
144
+ const entries = await safeReaddir(commandsDir);
145
+
146
+ for (const entry of entries) {
147
+ // .md 파일만 처리
148
+ if (!entry.endsWith('.md')) continue;
149
+
150
+ const entryPath = join(commandsDir, entry);
151
+ const stat = await safeStat(entryPath);
152
+
153
+ if (stat.isFile()) {
154
+ const command = await loadCommandFromFile(entryPath, source);
155
+ if (command) {
156
+ commands.push(command);
157
+ }
158
+ }
159
+ }
160
+ } catch (error) {
161
+ debugLog(`[discoverCommandsInDirectory] Error scanning ${commandsDir}: ${error.message}`);
162
+ }
163
+
164
+ return commands;
165
+ }
166
+
167
+ /**
168
+ * 모든 커스텀 커맨드를 검색합니다.
169
+ * Project 커맨드가 Personal 커맨드보다 우선순위가 높습니다.
170
+ * @returns {Promise<CustomCommand[]>}
171
+ */
172
+ export async function discoverAllCustomCommands() {
173
+ debugLog('[discoverAllCustomCommands] Starting command discovery...');
174
+
175
+ const allCommands = new Map(); // name -> command (중복 방지, 우선순위 적용)
176
+
177
+ // 1. Personal commands (낮은 우선순위)
178
+ const personalCommands = await discoverCommandsInDirectory(PERSONAL_COMMANDS_DIR, 'personal');
179
+ for (const command of personalCommands) {
180
+ allCommands.set(command.name, command);
181
+ }
182
+ debugLog(`[discoverAllCustomCommands] Found ${personalCommands.length} personal commands`);
183
+
184
+ // 2. Project commands (높은 우선순위 - 덮어씀)
185
+ const projectCommandsDir = join(process.cwd(), PROJECT_COMMANDS_DIR);
186
+ const projectCommands = await discoverCommandsInDirectory(projectCommandsDir, 'project');
187
+ for (const command of projectCommands) {
188
+ allCommands.set(command.name, command);
189
+ }
190
+ debugLog(`[discoverAllCustomCommands] Found ${projectCommands.length} project commands`);
191
+
192
+ const commands = Array.from(allCommands.values());
193
+ debugLog(`[discoverAllCustomCommands] Total unique commands: ${commands.length}`);
194
+
195
+ return commands;
196
+ }
197
+
198
+ /**
199
+ * 이름으로 커스텀 커맨드를 찾습니다.
200
+ * @param {string} commandName - 커맨드 이름
201
+ * @returns {Promise<CustomCommand|null>}
202
+ */
203
+ export async function findCustomCommandByName(commandName) {
204
+ debugLog(`[findCustomCommandByName] Looking for command: ${commandName}`);
205
+
206
+ // Project 커맨드 먼저 확인 (높은 우선순위)
207
+ const projectCommandPath = join(process.cwd(), PROJECT_COMMANDS_DIR, `${commandName}.md`);
208
+ try {
209
+ await safeAccess(projectCommandPath);
210
+ const command = await loadCommandFromFile(projectCommandPath, 'project');
211
+ if (command) {
212
+ debugLog(`[findCustomCommandByName] Found project command: ${commandName}`);
213
+ return command;
214
+ }
215
+ } catch {
216
+ // Project command not found, try personal
217
+ }
218
+
219
+ // Personal 커맨드 확인
220
+ const personalCommandPath = join(PERSONAL_COMMANDS_DIR, `${commandName}.md`);
221
+ try {
222
+ await safeAccess(personalCommandPath);
223
+ const command = await loadCommandFromFile(personalCommandPath, 'personal');
224
+ if (command) {
225
+ debugLog(`[findCustomCommandByName] Found personal command: ${commandName}`);
226
+ return command;
227
+ }
228
+ } catch {
229
+ // Personal command not found
230
+ }
231
+
232
+ debugLog(`[findCustomCommandByName] Command not found: ${commandName}`);
233
+ return null;
234
+ }
235
+
236
+ /**
237
+ * 커맨드 내용에서 $ARGUMENTS를 실제 인자로 치환합니다.
238
+ * @param {string} content - 커맨드 마크다운 내용
239
+ * @param {string} args - 사용자가 전달한 인자
240
+ * @returns {string}
241
+ */
242
+ export function substituteArguments(content, args) {
243
+ if (!args) return content;
244
+
245
+ // $ARGUMENTS 치환
246
+ let result = content.replace(/\$ARGUMENTS/g, args);
247
+
248
+ // ${CLAUDE_SESSION_ID} 등 환경변수 치환
249
+ result = result.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (match, varName) => {
250
+ return process.env[varName] || match;
251
+ });
252
+
253
+ // $ARGUMENTS가 없었으면 끝에 추가
254
+ if (result === content && args.trim()) {
255
+ result = content + '\n\nARGUMENTS: ' + args;
256
+ }
257
+
258
+ return result;
259
+ }
260
+
261
+ /**
262
+ * 쉘 명령어 플레이스홀더를 실행하고 결과로 치환합니다.
263
+ * 형식: !`command`
264
+ * @param {string} content - 커맨드 내용
265
+ * @returns {Promise<string>}
266
+ */
267
+ export async function executeShellPlaceholders(content) {
268
+ const shellRegex = /!\`([^`]+)\`/g;
269
+ const matches = [...content.matchAll(shellRegex)];
270
+
271
+ if (matches.length === 0) {
272
+ return content;
273
+ }
274
+
275
+ let result = content;
276
+
277
+ for (const match of matches) {
278
+ const fullMatch = match[0];
279
+ const command = match[1];
280
+
281
+ try {
282
+ const { execSync } = await import('child_process');
283
+ const output = execSync(command, {
284
+ encoding: 'utf8',
285
+ timeout: 30000,
286
+ maxBuffer: 1024 * 1024
287
+ }).trim();
288
+
289
+ result = result.replace(fullMatch, output);
290
+ debugLog(`[executeShellPlaceholders] Executed: ${command}`);
291
+ } catch (error) {
292
+ debugLog(`[executeShellPlaceholders] Failed to execute: ${command} - ${error.message}`);
293
+ result = result.replace(fullMatch, `[Error executing: ${command}]`);
294
+ }
295
+ }
296
+
297
+ return result;
298
+ }
299
+
300
+ /**
301
+ * 커스텀 커맨드를 로드하고 프롬프트로 변환합니다.
302
+ * @param {string} commandName - 커맨드 이름
303
+ * @param {string} [args] - 커맨드에 전달할 인자
304
+ * @returns {Promise<{ prompt: string, command: CustomCommand } | null>}
305
+ */
306
+ export async function loadCustomCommandAsPrompt(commandName, args = '') {
307
+ const command = await findCustomCommandByName(commandName);
308
+ if (!command) {
309
+ return null;
310
+ }
311
+
312
+ let prompt = command.content;
313
+
314
+ // 인자 치환
315
+ prompt = substituteArguments(prompt, args);
316
+
317
+ // 쉘 명령어 실행 및 치환
318
+ prompt = await executeShellPlaceholders(prompt);
319
+
320
+ debugLog(`[loadCustomCommandAsPrompt] Loaded command '${commandName}' with ${args ? 'args' : 'no args'}`);
321
+
322
+ return {
323
+ prompt,
324
+ command
325
+ };
326
+ }
327
+
328
+ /**
329
+ * 커스텀 커맨드 목록을 포맷팅하여 반환합니다.
330
+ * @returns {Promise<string>}
331
+ */
332
+ export async function formatCustomCommandList() {
333
+ const commands = await discoverAllCustomCommands();
334
+
335
+ if (commands.length === 0) {
336
+ return [
337
+ 'No custom commands found.',
338
+ '',
339
+ 'Command locations:',
340
+ ` Personal: ~/.aiexe/commands/<command-name>.md`,
341
+ ` Project: .aiexe/commands/<command-name>.md`,
342
+ '',
343
+ 'Create a command by making a .md file with optional YAML frontmatter.',
344
+ '',
345
+ 'Example (~/.aiexe/commands/review.md):',
346
+ '---',
347
+ 'name: review',
348
+ 'description: Review code for best practices',
349
+ 'argument-hint: [filename]',
350
+ '---',
351
+ '',
352
+ 'Review the following code for best practices:',
353
+ '$ARGUMENTS'
354
+ ].join('\n');
355
+ }
356
+
357
+ const lines = ['Custom Commands:', ''];
358
+
359
+ // 소스별 그룹화
360
+ const projectCommands = commands.filter(c => c.source === 'project');
361
+ const personalCommands = commands.filter(c => c.source === 'personal');
362
+
363
+ if (projectCommands.length > 0) {
364
+ lines.push('Project Commands (.aiexe/commands/):');
365
+ for (const cmd of projectCommands) {
366
+ const hint = cmd.argumentHint ? ` ${cmd.argumentHint}` : '';
367
+ const desc = cmd.description ? ` - ${cmd.description.substring(0, 50)}` : '';
368
+ lines.push(` /${cmd.name}${hint}${desc}`);
369
+ }
370
+ lines.push('');
371
+ }
372
+
373
+ if (personalCommands.length > 0) {
374
+ lines.push('Personal Commands (~/.aiexe/commands/):');
375
+ for (const cmd of personalCommands) {
376
+ const hint = cmd.argumentHint ? ` ${cmd.argumentHint}` : '';
377
+ const desc = cmd.description ? ` - ${cmd.description.substring(0, 50)}` : '';
378
+ lines.push(` /${cmd.name}${hint}${desc}`);
379
+ }
380
+ lines.push('');
381
+ }
382
+
383
+ lines.push('Usage: /<command-name> [arguments]');
384
+
385
+ return lines.join('\n');
386
+ }