aiexecode 1.0.71 → 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.

@@ -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
- stdout += data.toString();
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
- fileResults[filePath].push(lineContent);
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
- maxCount = DEFAULT_MAX_COUNT,
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 적용 (files_with_matches 모드에서만 적용)
333
- if (headLimit && headLimit > 0 && outputMode === 'files_with_matches') {
334
- results = results.slice(0, headLimit);
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: 'ripgrep을 사용하여 프로젝트 전역에서 파일 내용을 검색합니다. 정규식 패턴, 파일 타입 필터링, 다양한 출력 모드를 지원합니다.',
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 패턴으로 파일 필터링 (선택 사항, 예: "*.js", "**/*.tsx")',
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: '파일 타입 필터 (선택 사항, js, py, rust, go, java 등)',
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
- caseInsensitive: {
485
+ '-i': {
393
486
  type: 'boolean',
394
- description: '대소문자를 구분하지 않고 검색 (선택 사항, 기본값: false)',
487
+ description: 'Case insensitive search (rg -i)',
395
488
  },
396
- outputMode: {
489
+ output_mode: {
397
490
  type: 'string',
398
491
  enum: ['content', 'files_with_matches', 'count'],
399
- description: '출력 모드 (선택 사항, 기본값: files_with_matches): content(매칭된 라인), files_with_matches(파일 경로만), count(매칭 횟수)',
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
- contextBefore: {
494
+ '-B': {
402
495
  type: 'number',
403
- description: '매칭 N줄 표시 (선택 사항, content 모드에서만)',
496
+ description: 'Number of lines to show before each match (rg -B). Requires output_mode: "content", ignored otherwise.',
404
497
  },
405
- contextAfter: {
498
+ '-A': {
406
499
  type: 'number',
407
- description: '매칭 N줄 표시 (선택 사항, content 모드에서만)',
500
+ description: 'Number of lines to show after each match (rg -A). Requires output_mode: "content", ignored otherwise.',
408
501
  },
409
- context: {
502
+ '-C': {
410
503
  type: 'number',
411
- description: '매칭 전후 N줄 표시 (선택 사항, content 모드에서만)',
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
- maxCount: {
514
+ head_limit: {
418
515
  type: 'number',
419
- description: '파일당 최대 매칭 (선택 사항, 기본값: 500)',
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: '숨김 파일 포함 여부 (선택 사항, 기본값: false)',
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,4 +1,4 @@
1
- // MCP 설정 관리 유틸리티 - Claude Code 스타일
1
+ // MCP 설정 관리 유틸리티
2
2
  import { safeReadFile, safeWriteFile, safeMkdir } from './safe_fs.js';
3
3
  import { join } from 'path';
4
4
  import { createHash } from 'crypto';
@@ -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
- // 템플릿 변수가 제공된 경우, {{key}} 형태의 플레이스홀더를 치환
75
+ // EJS 템플릿 엔진을 사용하여 렌더링
75
76
  if (templateVars && typeof templateVars === 'object') {
76
- for (const [key, value] of Object.entries(templateVars)) {
77
- const placeholder = `{{${key}}}`;
78
- content = content.replaceAll(placeholder, value);
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