aiexecode 1.0.94 → 1.0.127
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 +310 -86
- package/mcp-agent-lib/src/mcp_message_logger.js +17 -16
- 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/prompts/completion_judge.txt +4 -0
- package/prompts/orchestrator.txt +116 -3
- package/src/LLMClient/client.js +401 -18
- package/src/LLMClient/converters/responses-to-claude.js +67 -18
- package/src/LLMClient/converters/responses-to-zai.js +667 -0
- package/src/LLMClient/errors.js +30 -4
- package/src/LLMClient/index.js +5 -0
- package/src/ai_based/completion_judge.js +263 -186
- package/src/ai_based/orchestrator.js +171 -35
- package/src/commands/agents.js +70 -0
- package/src/commands/apikey.js +1 -1
- package/src/commands/bg.js +129 -0
- package/src/commands/commands.js +51 -0
- package/src/commands/debug.js +52 -0
- package/src/commands/help.js +11 -1
- package/src/commands/model.js +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/config/feature_flags.js +6 -7
- package/src/frontend/App.js +108 -1
- package/src/frontend/components/AutocompleteMenu.js +7 -1
- package/src/frontend/components/BackgroundProcessList.js +175 -0
- package/src/frontend/components/ConversationItem.js +26 -10
- 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 +156 -12
- package/src/system/background_process.js +317 -0
- package/src/system/code_executer.js +496 -56
- package/src/system/command_parser.js +33 -3
- package/src/system/conversation_state.js +265 -0
- package/src/system/conversation_trimmer.js +132 -0
- package/src/system/custom_command_loader.js +386 -0
- package/src/system/file_integrity.js +73 -10
- package/src/system/log.js +10 -2
- package/src/system/output_helper.js +52 -9
- package/src/system/session.js +213 -58
- package/src/system/session_memory.js +30 -2
- package/src/system/skill_loader.js +318 -0
- package/src/system/system_info.js +254 -40
- package/src/system/tool_approval.js +10 -0
- package/src/system/tool_registry.js +15 -1
- package/src/system/ui_events.js +11 -0
- package/src/tools/code_editor.js +16 -10
- package/src/tools/file_reader.js +66 -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 +55 -2
- package/src/util/config_migration.js +174 -0
- package/src/util/debug_log.js +8 -2
- package/src/util/exit_handler.js +8 -0
- package/src/util/file_reference_parser.js +132 -0
- package/src/util/path_validator.js +178 -0
- package/src/util/prompt_loader.js +91 -1
- package/src/util/safe_fs.js +66 -3
- package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_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
|
+
}
|
|
@@ -5,7 +5,7 @@ import { safeReadFile, safeMkdir, safeAppendFile } from '../util/safe_fs.js';
|
|
|
5
5
|
import crypto from 'crypto';
|
|
6
6
|
import { dirname, join } from 'path';
|
|
7
7
|
import { createDebugLogger } from '../util/debug_log.js';
|
|
8
|
-
import { DEBUG_LOG_DIR } from '../util/config.js';
|
|
8
|
+
import { DEBUG_LOG_DIR, isDebugModeEnabled } from '../util/config.js';
|
|
9
9
|
|
|
10
10
|
const debugLog = createDebugLogger('file_integrity.log', 'file_integrity');
|
|
11
11
|
|
|
@@ -92,9 +92,11 @@ class FileIntegrityTracker {
|
|
|
92
92
|
debugLog(`Hash stored in session map`);
|
|
93
93
|
debugLog(`Total files tracked in this session: ${sessionFiles.size}`);
|
|
94
94
|
internalDebugLog.push(`[${timestamp}] Hash stored successfully`);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
if (isDebugModeEnabled()) {
|
|
96
|
+
const logFile = getLogFile();
|
|
97
|
+
await safeMkdir(dirname(logFile), { recursive: true }).catch(() => {});
|
|
98
|
+
await safeAppendFile(logFile, internalDebugLog.join('\n') + '\n').catch(() => {});
|
|
99
|
+
}
|
|
98
100
|
|
|
99
101
|
debugLog(`========== trackRead END ==========`);
|
|
100
102
|
debugLog(`[FileIntegrity] Tracked read: ${sessionID}:${filePath} (hash: ${hash.slice(0, 8)}...)`);
|
|
@@ -112,6 +114,38 @@ class FileIntegrityTracker {
|
|
|
112
114
|
return sessionFiles.get(filePath) || null;
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
/**
|
|
118
|
+
* 파일 콘텐츠 해시를 삭제합니다
|
|
119
|
+
* 대화가 trim되어 에이전트가 파일 내용을 알 수 없게 되었을 때 호출됩니다.
|
|
120
|
+
* 이렇게 하면 에이전트가 파일을 다시 읽기 전까지 편집할 수 없습니다.
|
|
121
|
+
* @param {string} sessionID - 세션 ID
|
|
122
|
+
* @param {string} filePath - 파일 경로
|
|
123
|
+
* @returns {boolean} 삭제 성공 여부
|
|
124
|
+
*/
|
|
125
|
+
clearContentHash(sessionID, filePath) {
|
|
126
|
+
debugLog(`========== clearContentHash START ==========`);
|
|
127
|
+
debugLog(`sessionID: ${sessionID}`);
|
|
128
|
+
debugLog(`filePath: ${filePath}`);
|
|
129
|
+
|
|
130
|
+
const sessionFiles = this.contentHashes.get(sessionID);
|
|
131
|
+
if (!sessionFiles) {
|
|
132
|
+
debugLog(`No session map found for session: ${sessionID}`);
|
|
133
|
+
debugLog(`========== clearContentHash END (NO SESSION) ==========`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const existed = sessionFiles.has(filePath);
|
|
138
|
+
if (existed) {
|
|
139
|
+
sessionFiles.delete(filePath);
|
|
140
|
+
debugLog(`Hash deleted for file: ${filePath}`);
|
|
141
|
+
} else {
|
|
142
|
+
debugLog(`No hash found for file: ${filePath}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
debugLog(`========== clearContentHash END ==========`);
|
|
146
|
+
return existed;
|
|
147
|
+
}
|
|
148
|
+
|
|
115
149
|
/**
|
|
116
150
|
* 파일 편집 전 무결성을 검증합니다
|
|
117
151
|
* @param {string} sessionID - 세션 ID
|
|
@@ -148,18 +182,22 @@ class FileIntegrityTracker {
|
|
|
148
182
|
assertDebugLog.push(`[${timestamp}] ERROR: No saved hash found`);
|
|
149
183
|
debugLog(`ERROR: No saved hash found for this file in session ${sessionID}`);
|
|
150
184
|
debugLog('========== assertIntegrity ERROR END ==========');
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
185
|
+
if (isDebugModeEnabled()) {
|
|
186
|
+
const logFile = getLogFile();
|
|
187
|
+
await safeMkdir(dirname(logFile), { recursive: true }).catch(() => {});
|
|
188
|
+
await safeAppendFile(logFile, assertDebugLog.join('\n') + '\n').catch(() => {});
|
|
189
|
+
}
|
|
154
190
|
|
|
155
191
|
throw new Error(
|
|
156
192
|
`You must read the file ${filePath} before editing it. Use a file reading tool first.`
|
|
157
193
|
);
|
|
158
194
|
}
|
|
159
195
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
196
|
+
if (isDebugModeEnabled()) {
|
|
197
|
+
const logFile = getLogFile();
|
|
198
|
+
await safeMkdir(dirname(logFile), { recursive: true }).catch(() => {});
|
|
199
|
+
await safeAppendFile(logFile, assertDebugLog.join('\n') + '\n').catch(() => {});
|
|
200
|
+
}
|
|
163
201
|
|
|
164
202
|
debugLog(`Reading current file content for comparison...`);
|
|
165
203
|
debugLog(` - Reading from path: ${filePath}`);
|
|
@@ -347,6 +385,31 @@ export async function assertFileIntegrity(filePath) {
|
|
|
347
385
|
debugLog('========== assertFileIntegrity (export wrapper) END ==========');
|
|
348
386
|
}
|
|
349
387
|
|
|
388
|
+
/**
|
|
389
|
+
* 파일 콘텐츠 해시를 삭제합니다 (현재 세션 기준)
|
|
390
|
+
* 대화가 trim되어 에이전트가 파일 내용을 알 수 없게 되었을 때 호출됩니다.
|
|
391
|
+
* @param {string} filePath - 파일 경로
|
|
392
|
+
* @returns {boolean} 삭제 성공 여부
|
|
393
|
+
*/
|
|
394
|
+
export function clearFileContentHash(filePath) {
|
|
395
|
+
debugLog(`========== clearFileContentHash (export wrapper) START ==========`);
|
|
396
|
+
debugLog(`filePath: ${filePath}`);
|
|
397
|
+
|
|
398
|
+
const sessionID = fileIntegrityTracker.getCurrentSession();
|
|
399
|
+
debugLog(`Current session ID: ${sessionID || 'NULL'}`);
|
|
400
|
+
|
|
401
|
+
if (!sessionID) {
|
|
402
|
+
debugLog(`ERROR: No current session set, cannot clear hash`);
|
|
403
|
+
debugLog('========== clearFileContentHash (export wrapper) END ==========');
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const result = fileIntegrityTracker.clearContentHash(sessionID, filePath);
|
|
408
|
+
debugLog(`Hash clear result: ${result}`);
|
|
409
|
+
debugLog('========== clearFileContentHash (export wrapper) END ==========');
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
|
|
350
413
|
/**
|
|
351
414
|
* 파일 스냅샷을 저장합니다 (현재 세션 기준)
|
|
352
415
|
* @param {string} filePath - 파일 경로
|
package/src/system/log.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { safeWriteFile, safeAccess, safeMkdir } from '../util/safe_fs.js';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { PAYLOAD_LOG_DIR } from '../util/config.js';
|
|
3
|
+
import { PAYLOAD_LOG_DIR, loadSettings } from '../util/config.js';
|
|
4
4
|
|
|
5
5
|
// 이 파일은 AI 요청과 응답을 보기 쉬운 로그 파일로 저장합니다.
|
|
6
6
|
// Planner·Orchestrator·Verifier의 대화 흐름을 모두 같은 위치에 남겨 재현성과 디버깅을 확보합니다.
|
|
7
|
+
// /debug on 설정 시에만 로그가 기록됩니다.
|
|
7
8
|
const LOG_DIR = PAYLOAD_LOG_DIR;
|
|
8
9
|
|
|
9
10
|
// 로그 폴더가 없으면 만들어서 나중에 파일을 쓸 수 있게 합니다.
|
|
@@ -41,14 +42,21 @@ function generateLogFileName(taskName) {
|
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* 로그 저장
|
|
45
|
+
* SHOW_API_PAYLOAD 설정이 true일 때만 로그를 기록합니다.
|
|
44
46
|
* @param {string} taskName - 태스크 이름
|
|
45
47
|
* @param {Object} data - JSON 데이터
|
|
46
48
|
* @param {string} provider - AI provider ('openai')
|
|
47
|
-
* @returns {Promise<string>} 생성된 로그 파일 경로
|
|
49
|
+
* @returns {Promise<string|null>} 생성된 로그 파일 경로 또는 null (디버그 모드 꺼짐)
|
|
48
50
|
*/
|
|
49
51
|
// 요청과 응답을 기록용 파일로 남기고 파일 경로를 되돌려줍니다.
|
|
50
52
|
export async function saveLog(taskName, data, provider = null) {
|
|
51
53
|
try {
|
|
54
|
+
// 디버그 모드가 꺼져있으면 로그를 기록하지 않음
|
|
55
|
+
const settings = await loadSettings();
|
|
56
|
+
if (settings?.SHOW_API_PAYLOAD !== true) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
await ensureLogDirectory();
|
|
53
61
|
|
|
54
62
|
const fileName = generateLogFileName(taskName);
|
|
@@ -1,35 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Output Helper - UI 이벤트 발생
|
|
3
|
+
* Pipe mode에서는 stderr로 출력하거나 무시
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import { uiEvents } from './ui_events.js';
|
|
6
7
|
|
|
8
|
+
// Pipe mode 감지
|
|
9
|
+
const isPipeMode = () => process.app_custom?.pipeMode === true;
|
|
10
|
+
|
|
7
11
|
export function logSystem(message) {
|
|
8
|
-
|
|
12
|
+
if (isPipeMode()) {
|
|
13
|
+
console.error(`[SYSTEM] ${message}`);
|
|
14
|
+
} else {
|
|
15
|
+
uiEvents.addSystemMessage(message);
|
|
16
|
+
}
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
export function logError(message) {
|
|
12
|
-
|
|
20
|
+
if (isPipeMode()) {
|
|
21
|
+
console.error(`[ERROR] ${message}`);
|
|
22
|
+
} else {
|
|
23
|
+
uiEvents.addErrorMessage(message);
|
|
24
|
+
}
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
export function logAssistantMessage(message) {
|
|
16
|
-
|
|
28
|
+
if (isPipeMode()) {
|
|
29
|
+
console.error(`[ASSISTANT] ${message}`);
|
|
30
|
+
} else {
|
|
31
|
+
uiEvents.addAssistantMessage(message);
|
|
32
|
+
}
|
|
17
33
|
}
|
|
18
34
|
|
|
19
35
|
export function logToolCall(toolName, args) {
|
|
20
|
-
|
|
36
|
+
if (isPipeMode()) {
|
|
37
|
+
console.error(`[TOOL] ${toolName}`);
|
|
38
|
+
} else {
|
|
39
|
+
uiEvents.startToolExecution(toolName, args);
|
|
40
|
+
}
|
|
21
41
|
}
|
|
22
42
|
|
|
23
43
|
export function logToolResult(toolName, stdout, originalResult = null, toolInput = null, fileSnapshot = null) {
|
|
24
|
-
|
|
44
|
+
if (isPipeMode()) {
|
|
45
|
+
// Pipe mode에서는 도구 결과를 간략하게 stderr로
|
|
46
|
+
const preview = stdout?.substring(0, 200) || '';
|
|
47
|
+
console.error(`[TOOL_RESULT] ${toolName}: ${preview}${stdout?.length > 200 ? '...' : ''}`);
|
|
48
|
+
} else {
|
|
49
|
+
uiEvents.addToolResult(toolName, { stdout, originalResult, fileSnapshot }, toolInput);
|
|
50
|
+
}
|
|
25
51
|
}
|
|
26
52
|
|
|
27
53
|
export function logCodeExecution(language, code) {
|
|
28
|
-
|
|
54
|
+
if (isPipeMode()) {
|
|
55
|
+
console.error(`[CODE] ${language}`);
|
|
56
|
+
} else {
|
|
57
|
+
uiEvents.startCodeExecution(language, code);
|
|
58
|
+
}
|
|
29
59
|
}
|
|
30
60
|
|
|
31
61
|
export function logCodeResult(stdout, stderr, exitCode) {
|
|
32
|
-
|
|
62
|
+
if (isPipeMode()) {
|
|
63
|
+
if (stdout) console.error(`[CODE_STDOUT] ${stdout.substring(0, 200)}${stdout.length > 200 ? '...' : ''}`);
|
|
64
|
+
if (stderr) console.error(`[CODE_STDERR] ${stderr.substring(0, 200)}${stderr.length > 200 ? '...' : ''}`);
|
|
65
|
+
} else {
|
|
66
|
+
uiEvents.addCodeResult(stdout, stderr, exitCode);
|
|
67
|
+
}
|
|
33
68
|
}
|
|
34
69
|
|
|
35
70
|
export function logIteration(iterationNumber) {
|
|
@@ -38,9 +73,17 @@ export function logIteration(iterationNumber) {
|
|
|
38
73
|
}
|
|
39
74
|
|
|
40
75
|
export function logMissionComplete() {
|
|
41
|
-
|
|
76
|
+
if (isPipeMode()) {
|
|
77
|
+
console.error('[COMPLETE] Mission completed');
|
|
78
|
+
} else {
|
|
79
|
+
uiEvents.missionCompleted(0);
|
|
80
|
+
}
|
|
42
81
|
}
|
|
43
82
|
|
|
44
83
|
export function logConversationRestored(count) {
|
|
45
|
-
|
|
84
|
+
if (isPipeMode()) {
|
|
85
|
+
console.error(`[RESTORED] ${count} conversation(s) restored`);
|
|
86
|
+
} else {
|
|
87
|
+
uiEvents.conversationRestored(count);
|
|
88
|
+
}
|
|
46
89
|
}
|