aiexecode 1.0.70 → 1.0.72
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/package.json +2 -1
- package/payload_viewer/out/404/index.html +1 -1
- package/payload_viewer/out/404.html +1 -1
- package/payload_viewer/out/index.html +1 -1
- package/payload_viewer/out/index.txt +1 -1
- package/prompts/completion_judge.txt +26 -0
- package/prompts/orchestrator.txt +172 -15
- package/src/ai_based/completion_judge.js +12 -1
- package/src/ai_based/orchestrator.js +23 -1
- package/src/cli/mcp_cli.js +1 -1
- package/src/frontend/App.js +21 -0
- package/src/frontend/components/TodoList.js +56 -0
- package/src/frontend/design/themeColors.js +4 -4
- package/src/system/session.js +53 -12
- package/src/system/session_memory.js +53 -2
- package/src/system/tool_registry.js +5 -2
- package/src/system/ui_events.js +10 -0
- package/src/tools/code_editor.js +2 -2
- package/src/tools/file_reader.js +6 -2
- package/src/tools/ripgrep.js +143 -50
- package/src/tools/todo_write.js +182 -0
- package/src/util/mcp_config_manager.js +1 -1
- package/src/util/prompt_loader.js +12 -4
- /package/payload_viewer/out/_next/static/{TvjwwIk8VOeA22CIg55Bx → 6yTW1SraROIP1ebN-kxTS}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{TvjwwIk8VOeA22CIg55Bx → 6yTW1SraROIP1ebN-kxTS}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{TvjwwIk8VOeA22CIg55Bx → 6yTW1SraROIP1ebN-kxTS}/_ssgManifest.js +0 -0
package/src/tools/ripgrep.js
CHANGED
|
@@ -5,6 +5,7 @@ import { theme } from '../frontend/design/themeColors.js';
|
|
|
5
5
|
|
|
6
6
|
const DEFAULT_TIMEOUT_MS = 120000;
|
|
7
7
|
const DEFAULT_MAX_COUNT = 500;
|
|
8
|
+
const MAX_OUTPUT_SIZE = 30000; // 30KB 출력 크기 제한
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* ripgrep 인수를 구성합니다
|
|
@@ -14,11 +15,12 @@ function buildRipgrepArgs({
|
|
|
14
15
|
path = null,
|
|
15
16
|
glob = null,
|
|
16
17
|
type = null,
|
|
17
|
-
caseInsensitive = false,
|
|
18
|
-
outputMode = 'files_with_matches',
|
|
19
|
-
contextBefore = 0,
|
|
20
|
-
contextAfter = 0,
|
|
21
|
-
context = 0,
|
|
18
|
+
'-i': caseInsensitive = false,
|
|
19
|
+
output_mode: outputMode = 'files_with_matches',
|
|
20
|
+
'-B': contextBefore = 0,
|
|
21
|
+
'-A': contextAfter = 0,
|
|
22
|
+
'-C': context = 0,
|
|
23
|
+
'-n': showLineNumbers = false,
|
|
22
24
|
multiline = false,
|
|
23
25
|
maxCount = DEFAULT_MAX_COUNT,
|
|
24
26
|
includeHidden = false
|
|
@@ -59,7 +61,7 @@ function buildRipgrepArgs({
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
// 최대 매치 수
|
|
64
|
+
// 최대 매치 수 (파일당)
|
|
63
65
|
args.push('--max-count', String(maxCount));
|
|
64
66
|
|
|
65
67
|
// 숨김 파일 포함
|
|
@@ -111,6 +113,8 @@ function executeRipgrep(args, cwd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
111
113
|
let stdout = '';
|
|
112
114
|
let stderr = '';
|
|
113
115
|
let isTimedOut = false;
|
|
116
|
+
let isSizeLimitExceeded = false;
|
|
117
|
+
let outputSize = 0;
|
|
114
118
|
let timeoutId;
|
|
115
119
|
let settled = false;
|
|
116
120
|
let rg;
|
|
@@ -164,7 +168,37 @@ function executeRipgrep(args, cwd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
164
168
|
}, timeoutMs);
|
|
165
169
|
|
|
166
170
|
rg.stdout.on('data', (data) => {
|
|
167
|
-
|
|
171
|
+
const dataStr = data.toString();
|
|
172
|
+
outputSize += dataStr.length;
|
|
173
|
+
|
|
174
|
+
// 출력 크기 제한 체크
|
|
175
|
+
if (outputSize > MAX_OUTPUT_SIZE && !isSizeLimitExceeded) {
|
|
176
|
+
isSizeLimitExceeded = true;
|
|
177
|
+
stdout += dataStr;
|
|
178
|
+
stdout += '\n[OUTPUT TRUNCATED: exceeded 30KB limit]';
|
|
179
|
+
|
|
180
|
+
// 프로세스 종료
|
|
181
|
+
try {
|
|
182
|
+
process.kill(-rg.pid, 'SIGTERM');
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
try {
|
|
185
|
+
process.kill(-rg.pid, 'SIGKILL');
|
|
186
|
+
} catch (_) {}
|
|
187
|
+
}, 1000);
|
|
188
|
+
} catch (_) {
|
|
189
|
+
rg.kill('SIGTERM');
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
try {
|
|
192
|
+
rg.kill('SIGKILL');
|
|
193
|
+
} catch (_) {}
|
|
194
|
+
}, 1000);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!isSizeLimitExceeded) {
|
|
200
|
+
stdout += dataStr;
|
|
201
|
+
}
|
|
168
202
|
});
|
|
169
203
|
|
|
170
204
|
rg.stderr.on('data', (data) => {
|
|
@@ -181,6 +215,15 @@ function executeRipgrep(args, cwd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
181
215
|
stderr: `${trimmedStderr}\n[TIMEOUT] ripgrep terminated due to timeout`.trim(),
|
|
182
216
|
code: 1,
|
|
183
217
|
timeout: true,
|
|
218
|
+
sizeLimitExceeded: isSizeLimitExceeded,
|
|
219
|
+
});
|
|
220
|
+
} else if (isSizeLimitExceeded) {
|
|
221
|
+
settle({
|
|
222
|
+
stdout: trimmedStdout,
|
|
223
|
+
stderr: trimmedStderr,
|
|
224
|
+
code: 0, // 정상 종료로 처리
|
|
225
|
+
timeout: false,
|
|
226
|
+
sizeLimitExceeded: true,
|
|
184
227
|
});
|
|
185
228
|
} else {
|
|
186
229
|
settle({
|
|
@@ -188,6 +231,7 @@ function executeRipgrep(args, cwd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
188
231
|
stderr: trimmedStderr,
|
|
189
232
|
code,
|
|
190
233
|
timeout: false,
|
|
234
|
+
sizeLimitExceeded: false,
|
|
191
235
|
});
|
|
192
236
|
}
|
|
193
237
|
});
|
|
@@ -198,6 +242,7 @@ function executeRipgrep(args, cwd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
198
242
|
stderr: isTimedOut ? `[TIMEOUT] ${err.message}` : err.message,
|
|
199
243
|
code: 1,
|
|
200
244
|
timeout: isTimedOut,
|
|
245
|
+
sizeLimitExceeded: isSizeLimitExceeded,
|
|
201
246
|
});
|
|
202
247
|
});
|
|
203
248
|
});
|
|
@@ -206,7 +251,7 @@ function executeRipgrep(args, cwd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
206
251
|
/**
|
|
207
252
|
* JSON 라인 출력을 파싱하여 파일별로 그룹화합니다
|
|
208
253
|
*/
|
|
209
|
-
function parseJsonOutput(buffer) {
|
|
254
|
+
function parseJsonOutput(buffer, showLineNumbers = false) {
|
|
210
255
|
const lines = buffer.split('\n').filter(Boolean);
|
|
211
256
|
const fileResults = {};
|
|
212
257
|
|
|
@@ -221,13 +266,19 @@ function parseJsonOutput(buffer) {
|
|
|
221
266
|
|
|
222
267
|
const filePath = data.path.text;
|
|
223
268
|
const lineContent = data.lines.text;
|
|
269
|
+
const lineNumber = data.line_number;
|
|
224
270
|
|
|
225
271
|
// 파일별로 그룹화
|
|
226
272
|
if (!fileResults[filePath]) {
|
|
227
273
|
fileResults[filePath] = [];
|
|
228
274
|
}
|
|
229
275
|
|
|
230
|
-
|
|
276
|
+
// 라인 번호 표시 옵션
|
|
277
|
+
if (showLineNumbers && lineNumber !== undefined) {
|
|
278
|
+
fileResults[filePath].push(`${lineNumber}:${lineContent}`);
|
|
279
|
+
} else {
|
|
280
|
+
fileResults[filePath].push(lineContent);
|
|
281
|
+
}
|
|
231
282
|
}
|
|
232
283
|
} catch (_) {
|
|
233
284
|
// JSON 파싱 실패 시 무시
|
|
@@ -271,15 +322,15 @@ export async function ripgrep({
|
|
|
271
322
|
path = null,
|
|
272
323
|
glob = null,
|
|
273
324
|
type = null,
|
|
274
|
-
caseInsensitive = false,
|
|
275
|
-
outputMode = 'files_with_matches',
|
|
276
|
-
contextBefore = 0,
|
|
277
|
-
contextAfter = 0,
|
|
278
|
-
context = 0,
|
|
325
|
+
'-i': caseInsensitive = false,
|
|
326
|
+
output_mode: outputMode = 'files_with_matches',
|
|
327
|
+
'-B': contextBefore = 0,
|
|
328
|
+
'-A': contextAfter = 0,
|
|
329
|
+
'-C': context = 0,
|
|
330
|
+
'-n': showLineNumbers = false,
|
|
279
331
|
multiline = false,
|
|
280
|
-
|
|
281
|
-
includeHidden = false
|
|
282
|
-
headLimit = null
|
|
332
|
+
head_limit: headLimit = null,
|
|
333
|
+
includeHidden = false
|
|
283
334
|
}) {
|
|
284
335
|
|
|
285
336
|
if (typeof pattern !== 'string' || !pattern.trim()) {
|
|
@@ -296,22 +347,32 @@ export async function ripgrep({
|
|
|
296
347
|
// ripgrep에 전달할 검색 경로 (상대 경로 그대로 전달)
|
|
297
348
|
const searchPath = path;
|
|
298
349
|
|
|
350
|
+
// maxCount 결정 (output_mode와 head_limit 기반)
|
|
351
|
+
let maxCount = DEFAULT_MAX_COUNT;
|
|
352
|
+
if (outputMode === 'content') {
|
|
353
|
+
maxCount = 100; // content 모드는 기본 100
|
|
354
|
+
}
|
|
355
|
+
if (headLimit && headLimit > 0 && headLimit < maxCount) {
|
|
356
|
+
maxCount = headLimit; // head_limit이 더 작으면 사용
|
|
357
|
+
}
|
|
358
|
+
|
|
299
359
|
const args = buildRipgrepArgs({
|
|
300
360
|
pattern: pattern.trim(),
|
|
301
361
|
path: searchPath,
|
|
302
362
|
glob,
|
|
303
363
|
type,
|
|
304
|
-
caseInsensitive,
|
|
305
|
-
outputMode,
|
|
306
|
-
contextBefore,
|
|
307
|
-
contextAfter,
|
|
308
|
-
context,
|
|
364
|
+
'-i': caseInsensitive,
|
|
365
|
+
output_mode: outputMode,
|
|
366
|
+
'-B': contextBefore,
|
|
367
|
+
'-A': contextAfter,
|
|
368
|
+
'-C': context,
|
|
369
|
+
'-n': showLineNumbers,
|
|
309
370
|
multiline,
|
|
310
371
|
maxCount,
|
|
311
372
|
includeHidden
|
|
312
373
|
});
|
|
313
374
|
|
|
314
|
-
const { stdout, stderr, code, timeout } = await executeRipgrep(args, resolvedCwd);
|
|
375
|
+
const { stdout, stderr, code, timeout, sizeLimitExceeded } = await executeRipgrep(args, resolvedCwd);
|
|
315
376
|
|
|
316
377
|
let results = [];
|
|
317
378
|
let totalMatches = 0;
|
|
@@ -324,14 +385,37 @@ export async function ripgrep({
|
|
|
324
385
|
results = parseCountOutput(stdout);
|
|
325
386
|
totalMatches = Object.values(results).reduce((sum, count) => sum + count, 0);
|
|
326
387
|
} else {
|
|
327
|
-
results = parseJsonOutput(stdout);
|
|
388
|
+
results = parseJsonOutput(stdout, showLineNumbers);
|
|
328
389
|
// 파일별 그룹화된 결과에서 총 매치 수 계산
|
|
329
390
|
totalMatches = Object.values(results).reduce((sum, matches) => sum + matches.length, 0);
|
|
330
391
|
}
|
|
331
392
|
|
|
332
|
-
// headLimit 적용 (
|
|
333
|
-
if (headLimit && headLimit > 0
|
|
334
|
-
|
|
393
|
+
// headLimit 적용 (모든 출력 모드)
|
|
394
|
+
if (headLimit && headLimit > 0) {
|
|
395
|
+
if (outputMode === 'files_with_matches') {
|
|
396
|
+
// 파일 목록 제한
|
|
397
|
+
results = results.slice(0, headLimit);
|
|
398
|
+
} else if (outputMode === 'count') {
|
|
399
|
+
// count 모드: 상위 N개 파일만
|
|
400
|
+
const entries = Object.entries(results).slice(0, headLimit);
|
|
401
|
+
results = Object.fromEntries(entries);
|
|
402
|
+
} else if (outputMode === 'content') {
|
|
403
|
+
// content 모드: 전체 매칭 라인을 headLimit까지만
|
|
404
|
+
const limitedResults = {};
|
|
405
|
+
let lineCount = 0;
|
|
406
|
+
|
|
407
|
+
for (const [file, matches] of Object.entries(results)) {
|
|
408
|
+
if (lineCount >= headLimit) break;
|
|
409
|
+
|
|
410
|
+
limitedResults[file] = [];
|
|
411
|
+
for (const match of matches) {
|
|
412
|
+
if (lineCount >= headLimit) break;
|
|
413
|
+
limitedResults[file].push(match);
|
|
414
|
+
lineCount++;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
results = limitedResults;
|
|
418
|
+
}
|
|
335
419
|
}
|
|
336
420
|
|
|
337
421
|
const noResult = !timeout && code === 1 && (
|
|
@@ -342,8 +426,12 @@ export async function ripgrep({
|
|
|
342
426
|
const success = !timeout && (code === 0 || noResult);
|
|
343
427
|
|
|
344
428
|
let errorMessage = '';
|
|
429
|
+
let warningMessage = '';
|
|
430
|
+
|
|
345
431
|
if (timeout) {
|
|
346
432
|
errorMessage = '검색 명령이 제한 시간을 초과했습니다.';
|
|
433
|
+
} else if (sizeLimitExceeded) {
|
|
434
|
+
warningMessage = '출력 크기가 30KB를 초과하여 결과가 잘렸습니다. 더 구체적인 검색어나 필터를 사용하세요.';
|
|
347
435
|
} else if (!success) {
|
|
348
436
|
errorMessage = stderr || 'ripgrep 실행 중 오류가 발생했습니다.';
|
|
349
437
|
} else if (noResult) {
|
|
@@ -358,6 +446,11 @@ export async function ripgrep({
|
|
|
358
446
|
pattern_used: pattern.trim(),
|
|
359
447
|
};
|
|
360
448
|
|
|
449
|
+
// 경고 메시지 추가
|
|
450
|
+
if (warningMessage) {
|
|
451
|
+
response.warning_message = warningMessage;
|
|
452
|
+
}
|
|
453
|
+
|
|
361
454
|
// 에러가 있을 경우에만 raw_stderr 포함
|
|
362
455
|
if (stderr) {
|
|
363
456
|
response.raw_stderr = stderr;
|
|
@@ -368,63 +461,63 @@ export async function ripgrep({
|
|
|
368
461
|
|
|
369
462
|
export const ripgrepSchema = {
|
|
370
463
|
name: 'ripgrep',
|
|
371
|
-
description: '
|
|
464
|
+
description: 'A powerful search tool built on ripgrep. Supports full regex syntax, file filtering with glob/type parameters, and multiple output modes.',
|
|
372
465
|
strict: false,
|
|
373
466
|
parameters: {
|
|
374
467
|
type: 'object',
|
|
375
468
|
properties: {
|
|
376
469
|
pattern: {
|
|
377
470
|
type: 'string',
|
|
378
|
-
description: '
|
|
471
|
+
description: 'The regular expression pattern to search for in file contents',
|
|
379
472
|
},
|
|
380
473
|
path: {
|
|
381
474
|
type: 'string',
|
|
382
|
-
description: '
|
|
475
|
+
description: 'File or directory to search in (rg PATH). Defaults to current working directory.',
|
|
383
476
|
},
|
|
384
477
|
glob: {
|
|
385
478
|
type: 'string',
|
|
386
|
-
description: 'Glob
|
|
479
|
+
description: 'Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}") - maps to rg --glob',
|
|
387
480
|
},
|
|
388
481
|
type: {
|
|
389
482
|
type: 'string',
|
|
390
|
-
description: '
|
|
483
|
+
description: 'File type to search (rg --type). Common types: js, py, rust, go, java, etc. More efficient than include for standard file types.',
|
|
391
484
|
},
|
|
392
|
-
|
|
485
|
+
'-i': {
|
|
393
486
|
type: 'boolean',
|
|
394
|
-
description: '
|
|
487
|
+
description: 'Case insensitive search (rg -i)',
|
|
395
488
|
},
|
|
396
|
-
|
|
489
|
+
output_mode: {
|
|
397
490
|
type: 'string',
|
|
398
491
|
enum: ['content', 'files_with_matches', 'count'],
|
|
399
|
-
description: '
|
|
492
|
+
description: 'Output mode: "content" shows matching lines (supports -A/-B/-C context, -n line numbers, head_limit), "files_with_matches" shows file paths (supports head_limit), "count" shows match counts (supports head_limit). Defaults to "files_with_matches".',
|
|
400
493
|
},
|
|
401
|
-
|
|
494
|
+
'-B': {
|
|
402
495
|
type: 'number',
|
|
403
|
-
description: '
|
|
496
|
+
description: 'Number of lines to show before each match (rg -B). Requires output_mode: "content", ignored otherwise.',
|
|
404
497
|
},
|
|
405
|
-
|
|
498
|
+
'-A': {
|
|
406
499
|
type: 'number',
|
|
407
|
-
description: '
|
|
500
|
+
description: 'Number of lines to show after each match (rg -A). Requires output_mode: "content", ignored otherwise.',
|
|
408
501
|
},
|
|
409
|
-
|
|
502
|
+
'-C': {
|
|
410
503
|
type: 'number',
|
|
411
|
-
description: '
|
|
504
|
+
description: 'Number of lines to show before and after each match (rg -C). Requires output_mode: "content", ignored otherwise.',
|
|
505
|
+
},
|
|
506
|
+
'-n': {
|
|
507
|
+
type: 'boolean',
|
|
508
|
+
description: 'Show line numbers in output (rg -n). Requires output_mode: "content", ignored otherwise.',
|
|
412
509
|
},
|
|
413
510
|
multiline: {
|
|
414
511
|
type: 'boolean',
|
|
415
|
-
description: '
|
|
512
|
+
description: 'Enable multiline mode where . matches newlines and patterns can span lines (rg -U --multiline-dotall). Default: false.',
|
|
416
513
|
},
|
|
417
|
-
|
|
514
|
+
head_limit: {
|
|
418
515
|
type: 'number',
|
|
419
|
-
description: '
|
|
516
|
+
description: 'Limit output to first N lines/entries, equivalent to "| head -N". Works across all output modes: content (limits output lines), files_with_matches (limits file paths), count (limits count entries). When unspecified, shows all results from ripgrep.',
|
|
420
517
|
},
|
|
421
518
|
includeHidden: {
|
|
422
519
|
type: 'boolean',
|
|
423
|
-
description: '
|
|
424
|
-
},
|
|
425
|
-
headLimit: {
|
|
426
|
-
type: 'number',
|
|
427
|
-
description: '출력 결과 수 제한 (선택 사항, 첫 N개만 반환)',
|
|
520
|
+
description: 'Include hidden files and directories in the search (rg --hidden). Default: false.',
|
|
428
521
|
},
|
|
429
522
|
},
|
|
430
523
|
required: ['pattern'],
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { uiEvents } from '../system/ui_events.js';
|
|
2
|
+
import { createDebugLogger } from '../util/debug_log.js';
|
|
3
|
+
import { theme } from '../frontend/design/themeColors.js';
|
|
4
|
+
import { updateCurrentTodos } from '../system/session_memory.js';
|
|
5
|
+
|
|
6
|
+
const debugLog = createDebugLogger('todo_write.log', 'todo_write');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Todo 관리 도구
|
|
10
|
+
* 현재 코딩 세션의 작업 목록을 생성하고 관리합니다.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Todo 리스트를 업데이트합니다
|
|
15
|
+
* @param {Object} params - 매개변수 객체
|
|
16
|
+
* @param {Array} params.todos - Todo 항목 배열
|
|
17
|
+
* @returns {Promise<Object>} 결과 객체
|
|
18
|
+
*/
|
|
19
|
+
export async function todo_write({ todos }) {
|
|
20
|
+
debugLog('========== todo_write START ==========');
|
|
21
|
+
debugLog(`Input todos count: ${todos?.length || 0}`);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// todos 배열 유효성 검증
|
|
25
|
+
if (!Array.isArray(todos)) {
|
|
26
|
+
debugLog('ERROR: todos is not an array');
|
|
27
|
+
debugLog('========== todo_write ERROR END ==========');
|
|
28
|
+
return {
|
|
29
|
+
operation_successful: false,
|
|
30
|
+
error_message: 'todos must be an array'
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 각 todo 항목 검증
|
|
35
|
+
for (let i = 0; i < todos.length; i++) {
|
|
36
|
+
const todo = todos[i];
|
|
37
|
+
debugLog(`Validating todo ${i + 1}/${todos.length}:`);
|
|
38
|
+
debugLog(` content: "${todo.content}"`);
|
|
39
|
+
debugLog(` status: "${todo.status}"`);
|
|
40
|
+
debugLog(` activeForm: "${todo.activeForm}"`);
|
|
41
|
+
|
|
42
|
+
if (!todo.content || typeof todo.content !== 'string') {
|
|
43
|
+
debugLog(`ERROR: todo ${i + 1} has invalid content`);
|
|
44
|
+
return {
|
|
45
|
+
operation_successful: false,
|
|
46
|
+
error_message: `Todo ${i + 1} has invalid content (must be a non-empty string)`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!todo.status || !['pending', 'in_progress', 'completed'].includes(todo.status)) {
|
|
51
|
+
debugLog(`ERROR: todo ${i + 1} has invalid status`);
|
|
52
|
+
return {
|
|
53
|
+
operation_successful: false,
|
|
54
|
+
error_message: `Todo ${i + 1} has invalid status (must be 'pending', 'in_progress', or 'completed')`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!todo.activeForm || typeof todo.activeForm !== 'string') {
|
|
59
|
+
debugLog(`ERROR: todo ${i + 1} has invalid activeForm`);
|
|
60
|
+
return {
|
|
61
|
+
operation_successful: false,
|
|
62
|
+
error_message: `Todo ${i + 1} has invalid activeForm (must be a non-empty string)`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// in_progress 상태가 정확히 하나인지 검증
|
|
68
|
+
const inProgressCount = todos.filter(t => t.status === 'in_progress').length;
|
|
69
|
+
debugLog(`in_progress count: ${inProgressCount}`);
|
|
70
|
+
|
|
71
|
+
if (inProgressCount !== 1) {
|
|
72
|
+
debugLog(`WARNING: Expected exactly 1 in_progress todo, found ${inProgressCount}`);
|
|
73
|
+
// 경고만 하고 계속 진행 (유연성을 위해)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 세션 메모리에 todos 저장
|
|
77
|
+
debugLog('Saving todos to session memory...');
|
|
78
|
+
updateCurrentTodos(todos);
|
|
79
|
+
debugLog('Todos saved to session memory');
|
|
80
|
+
|
|
81
|
+
// UI 이벤트로 todo 업데이트 전달
|
|
82
|
+
debugLog('Emitting todo update event...');
|
|
83
|
+
uiEvents.updateTodos(todos);
|
|
84
|
+
debugLog('Todo update event emitted');
|
|
85
|
+
|
|
86
|
+
debugLog('========== todo_write SUCCESS END ==========');
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
operation_successful: true,
|
|
90
|
+
todos_count: todos.length,
|
|
91
|
+
todos: todos,
|
|
92
|
+
in_progress_count: inProgressCount,
|
|
93
|
+
pending_count: todos.filter(t => t.status === 'pending').length,
|
|
94
|
+
completed_count: todos.filter(t => t.status === 'completed').length
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
} catch (error) {
|
|
98
|
+
debugLog(`========== todo_write EXCEPTION ==========`);
|
|
99
|
+
debugLog(`Exception caught: ${error.message}`);
|
|
100
|
+
debugLog(`Stack trace: ${error.stack}`);
|
|
101
|
+
debugLog('========== todo_write EXCEPTION END ==========');
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
operation_successful: false,
|
|
105
|
+
error_message: error.message
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const todoWriteSchema = {
|
|
111
|
+
"name": "todo_write",
|
|
112
|
+
"description": "Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.\nIt also helps the user understand the progress of the task and overall progress of their requests.\n\n## When to Use This Tool\nUse this tool proactively in these scenarios:\n\n1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions\n2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations\n3. User explicitly requests todo list - When the user directly asks you to use the todo list\n4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)\n5. After receiving new instructions - Immediately capture user requirements as todos\n6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time\n7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation\n\n## When NOT to Use This Tool\n\nSkip using this tool when:\n1. There is only a single, straightforward task\n2. The task is trivial and tracking it provides no organizational benefit\n3. The task can be completed in less than 3 trivial steps\n4. The task is purely conversational or informational\n\nNOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.\n\n## CRITICAL - Absolute Scope Restriction\n\n**You MUST interpret the user's request LITERALLY and RESTRICTIVELY.**\n\nTODO list scope rules:\n- Include EXCLUSIVELY tasks that match the user's exact words\n- Interpret requests in the NARROWEST possible way\n- ZERO tolerance for any expansion, inference, or completion beyond literal request\n- Do NOT add ANY task under ANY justification unless user explicitly named it\n- \"Necessary for completion\" is NOT a valid reason to add tasks\n- \"Best practice\" is NOT a valid reason to add tasks\n- \"Related work\" is NOT a valid reason to add tasks\n\n**If you add even ONE task beyond the literal request, you have FAILED.**\n\nThe user's request defines the MAXIMUM boundary - never exceed it.\n\n## Task States and Management\n\n1. **Task States**: Use these states to track progress:\n - pending: Task not yet started\n - in_progress: Currently working on (limit to ONE task at a time)\n - completed: Task finished successfully\n\n **IMPORTANT**: Task descriptions must have two forms:\n - content: The imperative form describing what needs to be done (e.g., \"Run tests\", \"Build the project\")\n - activeForm: The present continuous form shown during execution (e.g., \"Running tests\", \"Building the project\")\n\n2. **Task Management**:\n - Update task status in real-time as you work\n - Mark tasks complete IMMEDIATELY after finishing (don't batch completions)\n - Exactly ONE task must be in_progress at any time (not less, not more)\n - Complete current tasks before starting new ones\n - Remove tasks that are no longer relevant from the list entirely\n\n3. **Task Completion Requirements**:\n - ONLY mark a task as completed when you have FULLY accomplished it\n - If you encounter errors, blockers, or cannot finish, keep the task as in_progress\n - When blocked, create a new task describing what needs to be resolved\n - Never mark a task as completed if:\n - Tests are failing\n - Implementation is partial\n - You encountered unresolved errors\n - You couldn't find necessary files or dependencies\n\n4. **Task Breakdown**:\n - Create specific, actionable items\n - Break complex tasks into smaller, manageable steps\n - Use clear, descriptive task names\n - Always provide both forms:\n - content: \"Fix authentication bug\"\n - activeForm: \"Fixing authentication bug\"\n\nWhen in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.",
|
|
113
|
+
"strict": true,
|
|
114
|
+
"parameters": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"properties": {
|
|
117
|
+
"todos": {
|
|
118
|
+
"type": "array",
|
|
119
|
+
"description": "The updated todo list",
|
|
120
|
+
"items": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"properties": {
|
|
123
|
+
"content": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"description": "The imperative form describing what needs to be done (e.g., 'Run tests', 'Build the project')",
|
|
126
|
+
"minLength": 1
|
|
127
|
+
},
|
|
128
|
+
"status": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"description": "Task status: 'pending' (not yet started), 'in_progress' (currently working on), or 'completed' (finished successfully)",
|
|
131
|
+
"enum": ["pending", "in_progress", "completed"]
|
|
132
|
+
},
|
|
133
|
+
"activeForm": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"description": "The present continuous form shown during execution (e.g., 'Running tests', 'Building the project')",
|
|
136
|
+
"minLength": 1
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"required": ["content", "status", "activeForm"],
|
|
140
|
+
"additionalProperties": false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"required": ["todos"],
|
|
145
|
+
"additionalProperties": false
|
|
146
|
+
},
|
|
147
|
+
"ui_display": {
|
|
148
|
+
"show_tool_call": true,
|
|
149
|
+
"show_tool_result": true,
|
|
150
|
+
"display_name": "Todo",
|
|
151
|
+
"format_tool_call": (args) => {
|
|
152
|
+
const todos = args.todos || [];
|
|
153
|
+
const inProgress = todos.filter(t => t.status === 'in_progress').length;
|
|
154
|
+
const completed = todos.filter(t => t.status === 'completed').length;
|
|
155
|
+
const pending = todos.filter(t => t.status === 'pending').length;
|
|
156
|
+
return `(${todos.length} tasks: ${completed} done, ${inProgress} active, ${pending} pending)`;
|
|
157
|
+
},
|
|
158
|
+
"format_tool_result": (result) => {
|
|
159
|
+
if (result.operation_successful) {
|
|
160
|
+
const total = result.todos_count || 0;
|
|
161
|
+
const completed = result.completed_count || 0;
|
|
162
|
+
const inProgress = result.in_progress_count || 0;
|
|
163
|
+
const pending = result.pending_count || 0;
|
|
164
|
+
return {
|
|
165
|
+
type: 'formatted',
|
|
166
|
+
parts: [
|
|
167
|
+
{ text: 'Updated ', style: {} },
|
|
168
|
+
{ text: String(total), style: { color: theme.brand.light, bold: true } },
|
|
169
|
+
{ text: ` task${total !== 1 ? 's' : ''} `, style: {} },
|
|
170
|
+
{ text: `(${completed} done, ${inProgress} active, ${pending} pending)`, style: { color: theme.text.dim } }
|
|
171
|
+
]
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return result.error_message || 'Error updating todos';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// 함수 맵 - 문자열로 함수 호출 가능
|
|
180
|
+
export const TODO_WRITE_FUNCTIONS = {
|
|
181
|
+
'todo_write': todo_write
|
|
182
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { safeReadFile, safeAccess } from "./safe_fs.js";
|
|
2
2
|
import { join, dirname } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
+
import ejs from "ejs";
|
|
4
5
|
|
|
5
6
|
const moduleDirname = dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
const defaultProjectRoot = dirname(dirname(moduleDirname));
|
|
@@ -71,11 +72,18 @@ async function loadPromptFromPromptsDir(promptFileName) {
|
|
|
71
72
|
export async function createSystemMessage(promptFileName, templateVars = {}) {
|
|
72
73
|
let content = await loadPromptFromPromptsDir(promptFileName);
|
|
73
74
|
|
|
74
|
-
// 템플릿
|
|
75
|
+
// EJS 템플릿 엔진을 사용하여 렌더링
|
|
75
76
|
if (templateVars && typeof templateVars === 'object') {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
try {
|
|
78
|
+
content = ejs.render(content, templateVars, {
|
|
79
|
+
// EJS 옵션
|
|
80
|
+
delimiter: '%', // <% %> 구문 사용
|
|
81
|
+
openDelimiter: '<',
|
|
82
|
+
closeDelimiter: '>'
|
|
83
|
+
});
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// EJS 렌더링 실패 시 원본 content 유지
|
|
86
|
+
console.error(`EJS rendering failed for ${promptFileName}:`, err.message);
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|