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,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
|
+
}
|
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
|
};
|