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.

Files changed (60) hide show
  1. package/README.md +198 -88
  2. package/index.js +43 -9
  3. package/package.json +4 -4
  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 +30 -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/apikey.js +1 -1
  20. package/src/commands/commands.js +51 -0
  21. package/src/commands/debug.js +52 -0
  22. package/src/commands/help.js +11 -1
  23. package/src/commands/model.js +42 -7
  24. package/src/commands/reasoning_effort.js +2 -2
  25. package/src/commands/skills.js +46 -0
  26. package/src/config/ai_models.js +106 -6
  27. package/src/config/constants.js +71 -0
  28. package/src/frontend/App.js +8 -0
  29. package/src/frontend/components/AutocompleteMenu.js +7 -1
  30. package/src/frontend/components/CurrentModelView.js +2 -2
  31. package/src/frontend/components/HelpView.js +106 -2
  32. package/src/frontend/components/Input.js +33 -11
  33. package/src/frontend/components/ModelListView.js +1 -1
  34. package/src/frontend/components/SetupWizard.js +51 -8
  35. package/src/frontend/hooks/useFileCompletion.js +467 -0
  36. package/src/frontend/utils/toolUIFormatter.js +261 -0
  37. package/src/system/agents_loader.js +289 -0
  38. package/src/system/ai_request.js +175 -12
  39. package/src/system/command_parser.js +33 -3
  40. package/src/system/conversation_state.js +265 -0
  41. package/src/system/custom_command_loader.js +386 -0
  42. package/src/system/session.js +59 -35
  43. package/src/system/skill_loader.js +318 -0
  44. package/src/system/tool_approval.js +10 -0
  45. package/src/tools/file_reader.js +49 -9
  46. package/src/tools/glob.js +0 -3
  47. package/src/tools/ripgrep.js +5 -7
  48. package/src/tools/skill_tool.js +122 -0
  49. package/src/tools/web_downloader.js +0 -3
  50. package/src/util/clone.js +174 -0
  51. package/src/util/config.js +38 -2
  52. package/src/util/config_migration.js +174 -0
  53. package/src/util/file_reference_parser.js +132 -0
  54. package/src/util/path_validator.js +178 -0
  55. package/src/util/prompt_loader.js +68 -1
  56. package/src/util/safe_fs.js +43 -3
  57. package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
  58. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_buildManifest.js +0 -0
  59. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_clientMiddlewareManifest.json +0 -0
  60. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_ssgManifest.js +0 -0
@@ -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
+ }
@@ -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
  };