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.
Files changed (80) hide show
  1. package/README.md +198 -88
  2. package/index.js +310 -86
  3. package/mcp-agent-lib/src/mcp_message_logger.js +17 -16
  4. package/package.json +4 -4
  5. package/payload_viewer/out/404/index.html +1 -1
  6. package/payload_viewer/out/404.html +1 -1
  7. package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
  8. package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
  9. package/payload_viewer/out/index.html +1 -1
  10. package/payload_viewer/out/index.txt +3 -3
  11. package/payload_viewer/web_server.js +361 -0
  12. package/prompts/completion_judge.txt +4 -0
  13. package/prompts/orchestrator.txt +116 -3
  14. package/src/LLMClient/client.js +401 -18
  15. package/src/LLMClient/converters/responses-to-claude.js +67 -18
  16. package/src/LLMClient/converters/responses-to-zai.js +667 -0
  17. package/src/LLMClient/errors.js +30 -4
  18. package/src/LLMClient/index.js +5 -0
  19. package/src/ai_based/completion_judge.js +263 -186
  20. package/src/ai_based/orchestrator.js +171 -35
  21. package/src/commands/agents.js +70 -0
  22. package/src/commands/apikey.js +1 -1
  23. package/src/commands/bg.js +129 -0
  24. package/src/commands/commands.js +51 -0
  25. package/src/commands/debug.js +52 -0
  26. package/src/commands/help.js +11 -1
  27. package/src/commands/model.js +42 -7
  28. package/src/commands/reasoning_effort.js +2 -2
  29. package/src/commands/skills.js +46 -0
  30. package/src/config/ai_models.js +106 -6
  31. package/src/config/constants.js +71 -0
  32. package/src/config/feature_flags.js +6 -7
  33. package/src/frontend/App.js +108 -1
  34. package/src/frontend/components/AutocompleteMenu.js +7 -1
  35. package/src/frontend/components/BackgroundProcessList.js +175 -0
  36. package/src/frontend/components/ConversationItem.js +26 -10
  37. package/src/frontend/components/CurrentModelView.js +2 -2
  38. package/src/frontend/components/HelpView.js +106 -2
  39. package/src/frontend/components/Input.js +33 -11
  40. package/src/frontend/components/ModelListView.js +1 -1
  41. package/src/frontend/components/SetupWizard.js +51 -8
  42. package/src/frontend/hooks/useFileCompletion.js +467 -0
  43. package/src/frontend/utils/toolUIFormatter.js +261 -0
  44. package/src/system/agents_loader.js +289 -0
  45. package/src/system/ai_request.js +156 -12
  46. package/src/system/background_process.js +317 -0
  47. package/src/system/code_executer.js +496 -56
  48. package/src/system/command_parser.js +33 -3
  49. package/src/system/conversation_state.js +265 -0
  50. package/src/system/conversation_trimmer.js +132 -0
  51. package/src/system/custom_command_loader.js +386 -0
  52. package/src/system/file_integrity.js +73 -10
  53. package/src/system/log.js +10 -2
  54. package/src/system/output_helper.js +52 -9
  55. package/src/system/session.js +213 -58
  56. package/src/system/session_memory.js +30 -2
  57. package/src/system/skill_loader.js +318 -0
  58. package/src/system/system_info.js +254 -40
  59. package/src/system/tool_approval.js +10 -0
  60. package/src/system/tool_registry.js +15 -1
  61. package/src/system/ui_events.js +11 -0
  62. package/src/tools/code_editor.js +16 -10
  63. package/src/tools/file_reader.js +66 -9
  64. package/src/tools/glob.js +0 -3
  65. package/src/tools/ripgrep.js +5 -7
  66. package/src/tools/skill_tool.js +122 -0
  67. package/src/tools/web_downloader.js +0 -3
  68. package/src/util/clone.js +174 -0
  69. package/src/util/config.js +55 -2
  70. package/src/util/config_migration.js +174 -0
  71. package/src/util/debug_log.js +8 -2
  72. package/src/util/exit_handler.js +8 -0
  73. package/src/util/file_reference_parser.js +132 -0
  74. package/src/util/path_validator.js +178 -0
  75. package/src/util/prompt_loader.js +91 -1
  76. package/src/util/safe_fs.js +66 -3
  77. package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
  78. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_buildManifest.js +0 -0
  79. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_clientMiddlewareManifest.json +0 -0
  80. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_ssgManifest.js +0 -0
@@ -2,9 +2,10 @@ import dotenv from "dotenv";
2
2
  // 이 파일은 계획을 실제 행동으로 옮기기 위해 어떤 도구를 호출할지 결정합니다.
3
3
  import path from 'path';
4
4
  import { truncateWithOmit } from "../util/text_formatter.js";
5
- import { createSystemMessage } from "../util/prompt_loader.js";
5
+ import { createSystemMessage, createTodoReminder, createSystemReminder, createTrimmedFileReminder } from "../util/prompt_loader.js";
6
6
  import { request, shouldRetryWithTrim, getModelForProvider } from "../system/ai_request.js";
7
- import { cleanupOrphanOutputs, trimConversation } from "../system/conversation_trimmer.js";
7
+ import { supportsCaching } from "../config/ai_models.js";
8
+ import { cleanupOrphanOutputs, trimConversation, getTrimmedFileReads, clearTrimmedFileReads } from "../system/conversation_trimmer.js";
8
9
  import { runPythonCodeSchema, bashSchema } from "../system/code_executer.js";
9
10
  import { readFileSchema, readFileRangeSchema } from "../tools/file_reader.js";
10
11
  import { writeFileSchema, editFileRangeSchema, editFileReplaceSchema } from "../tools/code_editor.js";
@@ -13,9 +14,11 @@ import { responseMessageSchema } from "../tools/response_message.js";
13
14
  import { todoWriteSchema } from "../tools/todo_write.js";
14
15
  import { ripgrepSchema } from "../tools/ripgrep.js";
15
16
  import { globSearchSchema } from "../tools/glob.js";
17
+ import { skillSchema, generateSkillListForPrompt } from "../tools/skill_tool.js";
16
18
  import { loadSettings } from "../util/config.js";
17
19
  import { createDebugLogger } from "../util/debug_log.js";
18
20
  import { getCurrentTodos } from "../system/session_memory.js";
21
+ import { loadAgentsMdForPrompt } from "../system/agents_loader.js";
19
22
  dotenv.config({ quiet: true });
20
23
 
21
24
  const debugLog = createDebugLogger('orchestrator.log', 'orchestrator');
@@ -26,42 +29,144 @@ let orchestratorRequestOptions = null;
26
29
  /**
27
30
  * Orchestrator conversation을 초기화하거나 시스템 프롬프트를 업데이트합니다.
28
31
  * 매 요청마다 프롬프트 파일을 새로 읽어서 변경사항을 즉시 반영합니다.
32
+ *
33
+ * 로그 분석에서 발견한 전략 적용:
34
+ * 1. 캐시 제어 (cache_control: ephemeral) - Z.AI/GLM 모델의 경우
35
+ * 2. System-Reminder 패턴 - TODO 상태 등을 별도 블록으로 분리
36
+ * 3. 다중 시스템 메시지 구조 - 캐시 효과 극대화
29
37
  */
30
38
  async function ensureConversationInitialized() {
31
- // 매번 최신 시스템 프롬프트를 로드
39
+ // 현재 모델 확인 (캐시 제어 여부 결정)
40
+ const currentModel = await getModelForProvider();
41
+ const useCache = supportsCaching(currentModel);
42
+ debugLog(`[ensureConversationInitialized] Model: ${currentModel}, Cache enabled: ${useCache}`);
43
+
44
+ // 매번 최신 시스템 프롬프트를 로드 (캐시 제어 옵션 적용)
32
45
  const systemMessage = await createSystemMessage("orchestrator.txt", {
33
46
  CWD: process.cwd(),
34
47
  OS: process.app_custom?.systemInfo?.os || 'unknown'
48
+ }, {
49
+ model: currentModel,
50
+ enableCache: useCache
35
51
  });
36
52
 
37
53
  // 현재 todos 가져오기
38
54
  const currentTodos = getCurrentTodos();
39
55
 
40
- // todos를 system message에 추가
41
- let systemMessageText = systemMessage.content;
42
- if (currentTodos && currentTodos.length > 0) {
43
- const todosSection = '\n\n## Current Task List (Your TODO tracker)\n\n' +
44
- 'You have created the following task list to track your work. Continue working on these tasks systematically:\n\n' +
45
- currentTodos.map((todo, index) => {
46
- const statusIcon = todo.status === 'completed' ? '✓' :
47
- todo.status === 'in_progress' ? '→' : '○';
48
- return `${index + 1}. [${statusIcon}] ${todo.content} (${todo.status})`;
49
- }).join('\n') +
50
- '\n\n**Remember**: Mark tasks as completed immediately after finishing them. Keep exactly ONE task as in_progress at a time.\n';
51
-
52
- systemMessageText += todosSection;
53
- debugLog(`[ensureConversationInitialized] Added ${currentTodos.length} todos to system message`);
54
- }
56
+ // 사용 가능한 스킬 목록 생성
57
+ const skillListText = await generateSkillListForPrompt();
58
+ debugLog(`[ensureConversationInitialized] Generated skill list: ${skillListText ? 'has skills' : 'no skills'}`);
59
+
60
+ // AGENTS.md 로드 (프로젝트별 에이전트 지침)
61
+ const agentsMdText = await loadAgentsMdForPrompt();
62
+ debugLog(`[ensureConversationInitialized] AGENTS.md: ${agentsMdText ? `loaded (${agentsMdText.length} chars)` : 'not found'}`);
63
+
64
+ // 시스템 메시지 구성 (캐싱 모델의 경우 다중 블록 구조)
65
+ let systemMessageEntry;
66
+
67
+ if (useCache && Array.isArray(systemMessage.content)) {
68
+ // 캐싱 모델: 다중 블록 구조 사용 (Claude Code 스타일)
69
+ const contentBlocks = [...systemMessage.content];
70
+
71
+ // AGENTS.md를 별도 블록으로 추가 (프로젝트별 지침)
72
+ if (agentsMdText) {
73
+ contentBlocks.push({
74
+ type: "input_text",
75
+ text: agentsMdText,
76
+ cache_control: { type: "ephemeral" } // 프로젝트가 바뀔 때만 변경되므로 캐시
77
+ });
78
+ debugLog(`[ensureConversationInitialized] Added AGENTS.md as separate block (cached)`);
79
+ }
80
+
81
+ // 스킬 목록을 별도 블록으로 추가
82
+ if (skillListText) {
83
+ contentBlocks.push({
84
+ type: "input_text",
85
+ text: skillListText
86
+ // 스킬 목록은 자주 변경될 수 있으므로 캐시하지 않음
87
+ });
88
+ debugLog(`[ensureConversationInitialized] Added skill list as separate block`);
89
+ }
55
90
 
56
- const systemMessageEntry = {
57
- role: "system",
58
- content: [
59
- {
91
+ // TODO 리마인더를 별도 블록으로 추가 (캐시 대상 아님)
92
+ if (currentTodos && currentTodos.length > 0) {
93
+ const todoReminder = createTodoReminder(currentTodos);
94
+ contentBlocks.push({
60
95
  type: "input_text",
61
- text: systemMessageText
96
+ text: todoReminder
97
+ // cache_control 없음 - 동적 데이터이므로
98
+ });
99
+ debugLog(`[ensureConversationInitialized] Added ${currentTodos.length} todos as separate block (uncached)`);
100
+ }
101
+
102
+ // Trim된 파일 읽기 알림 추가 (파일 경로 알림 기능)
103
+ const trimmedFiles = getTrimmedFileReads();
104
+ if (trimmedFiles.length > 0) {
105
+ const trimmedFileReminder = createTrimmedFileReminder(trimmedFiles);
106
+ if (trimmedFileReminder) {
107
+ contentBlocks.push({
108
+ type: "input_text",
109
+ text: trimmedFileReminder
110
+ // cache_control 없음 - 동적 데이터이므로
111
+ });
112
+ debugLog(`[ensureConversationInitialized] Added trimmed file reminder for ${trimmedFiles.length} files (uncached)`);
62
113
  }
63
- ]
64
- };
114
+ }
115
+
116
+ systemMessageEntry = {
117
+ role: "system",
118
+ content: contentBlocks
119
+ };
120
+ } else {
121
+ // 일반 모델: 기존 방식 유지
122
+ let systemMessageText = systemMessage.content;
123
+
124
+ // AGENTS.md 추가 (프로젝트별 지침)
125
+ if (agentsMdText) {
126
+ systemMessageText += agentsMdText;
127
+ debugLog(`[ensureConversationInitialized] Added AGENTS.md to system message`);
128
+ }
129
+
130
+ // 스킬 목록 추가
131
+ if (skillListText) {
132
+ systemMessageText += skillListText;
133
+ debugLog(`[ensureConversationInitialized] Added skill list to system message`);
134
+ }
135
+
136
+ if (currentTodos && currentTodos.length > 0) {
137
+ const todosSection = '\n\n## Current Task List (Your TODO tracker)\n\n' +
138
+ 'You have created the following task list to track your work. Continue working on these tasks systematically:\n\n' +
139
+ currentTodos.map((todo, index) => {
140
+ const statusIcon = todo.status === 'completed' ? '✓' :
141
+ todo.status === 'in_progress' ? '→' : '○';
142
+ return `${index + 1}. [${statusIcon}] ${todo.content} (${todo.status})`;
143
+ }).join('\n') +
144
+ '\n\n**Remember**: Mark tasks as completed immediately after finishing them. Keep exactly ONE task as in_progress at a time.\n';
145
+
146
+ systemMessageText += todosSection;
147
+ debugLog(`[ensureConversationInitialized] Added ${currentTodos.length} todos to system message`);
148
+ }
149
+
150
+ // Trim된 파일 읽기 알림 추가 (파일 경로 알림 기능)
151
+ const trimmedFiles = getTrimmedFileReads();
152
+ if (trimmedFiles.length > 0) {
153
+ const trimmedFileReminder = createTrimmedFileReminder(trimmedFiles);
154
+ if (trimmedFileReminder) {
155
+ systemMessageText += '\n\n' + trimmedFileReminder;
156
+ debugLog(`[ensureConversationInitialized] Added trimmed file reminder for ${trimmedFiles.length} files`);
157
+ }
158
+ }
159
+
160
+ systemMessageEntry = {
161
+ role: "system",
162
+ content: [
163
+ {
164
+ type: "input_text",
165
+ text: systemMessageText
166
+ }
167
+ ]
168
+ };
169
+ }
65
170
 
66
171
  // conversation이 비어있으면 새로 추가
67
172
  if (!orchestratorConversation.length) {
@@ -97,6 +202,7 @@ function appendResponseToConversation(response) {
97
202
  export function resetOrchestratorConversation() {
98
203
  orchestratorConversation.length = 0;
99
204
  orchestratorRequestOptions = null;
205
+ clearTrimmedFileReads(); // 세션 초기화 시 trim된 파일 목록도 초기화
100
206
  }
101
207
 
102
208
  export function getOrchestratorConversation() {
@@ -201,11 +307,13 @@ async function dispatchOrchestratorRequest({ toolChoice }) {
201
307
  const estimatedTokens = Math.ceil(conversationJson.length / 4); // 대략 4 chars = 1 token
202
308
  debugLog(`[dispatchOrchestratorRequest] Estimated tokens: ${estimatedTokens} (conversation size: ${conversationJson.length} bytes)`);
203
309
 
204
- debugLog(`[dispatchOrchestratorRequest] Sending API request...`);
310
+ const apiStartTime = Date.now();
311
+ debugLog(`[dispatchOrchestratorRequest] >>>>> Sending API request at ${new Date(apiStartTime).toISOString()}...`);
205
312
 
206
313
  try {
207
314
  const response = await request(taskName, requestConfig);
208
- debugLog(`[dispatchOrchestratorRequest] API request successful`);
315
+ const apiDuration = Date.now() - apiStartTime;
316
+ debugLog(`[dispatchOrchestratorRequest] <<<<< API request successful in ${apiDuration}ms`);
209
317
  debugLog(`[dispatchOrchestratorRequest] Response outputs: ${response?.output?.length || 0}`);
210
318
  if (response?.output) {
211
319
  const outputTypes = {};
@@ -219,7 +327,8 @@ async function dispatchOrchestratorRequest({ toolChoice }) {
219
327
  // consolelog(JSON.stringify(response, null, 2));
220
328
  return response;
221
329
  } catch (error) {
222
- debugLog(`[dispatchOrchestratorRequest] API request failed: ${error.message}`);
330
+ const apiDuration = Date.now() - apiStartTime;
331
+ debugLog(`[dispatchOrchestratorRequest] <<<<< API request FAILED after ${apiDuration}ms: ${error.message}`);
223
332
  debugLog(`[dispatchOrchestratorRequest] Error name: ${error.name}, type: ${error?.constructor?.name}`);
224
333
 
225
334
  // AbortError는 즉시 전파 (세션 중단)
@@ -308,7 +417,8 @@ export async function orchestrateMission({ improvement_points = '', mcpToolSchem
308
417
  response_message: responseMessageSchema,
309
418
  todo_write: todoWriteSchema,
310
419
  ripgrep: ripgrepSchema,
311
- glob_search: globSearchSchema
420
+ glob_search: globSearchSchema,
421
+ invoke_skill: skillSchema // 스킬 호출 도구
312
422
  };
313
423
 
314
424
  // Python 관련 도구 (조건부 추가)
@@ -355,14 +465,40 @@ export async function orchestrateMission({ improvement_points = '', mcpToolSchem
355
465
  debugLog(`[orchestrateMission] Conversation initialized, current length: ${orchestratorConversation.length}`);
356
466
 
357
467
  debugLog(`[orchestrateMission] Adding user message to conversation: "${improvementPointsText.substring(0, 100)}"`);
468
+
469
+ // 캐싱 모델인 경우 사용자 메시지에도 캐시 제어 적용 (Claude Code 스타일)
470
+ const useCache = supportsCaching(model);
471
+ const userContentBlocks = [];
472
+
473
+ // Auto-generated 메시지의 경우 system-reminder 추가
474
+ if (isAutoGenerated) {
475
+ const autoGenReminder = createSystemReminder(
476
+ '이 메시지는 시스템에서 자동 생성되었습니다. 사용자가 직접 입력한 것이 아닙니다.',
477
+ { hideFromUser: true }
478
+ );
479
+ userContentBlocks.push({
480
+ type: "input_text",
481
+ text: autoGenReminder
482
+ });
483
+ }
484
+
485
+ // 메인 사용자 메시지
486
+ const mainContent = {
487
+ type: "input_text",
488
+ text: improvementPointsText
489
+ };
490
+
491
+ // 캐싱 모델의 경우 사용자 메시지에도 캐시 적용 (반복되는 긴 메시지에 효과적)
492
+ if (useCache && improvementPointsText.length > 500) {
493
+ mainContent.cache_control = { type: "ephemeral" };
494
+ debugLog(`[orchestrateMission] Applied cache control to user message (${improvementPointsText.length} chars)`);
495
+ }
496
+
497
+ userContentBlocks.push(mainContent);
498
+
358
499
  const userMessage = {
359
500
  role: "user",
360
- content: [
361
- {
362
- type: "input_text",
363
- text: improvementPointsText
364
- }
365
- ]
501
+ content: userContentBlocks
366
502
  };
367
503
 
368
504
  // Auto-generated user message인 경우 _internal_only 플래그 추가
@@ -0,0 +1,70 @@
1
+ import { uiEvents } from '../system/ui_events.js';
2
+ import {
3
+ formatAgentsMdSummary,
4
+ discoverAllAgentsMd
5
+ } from '../system/agents_loader.js';
6
+ import { safeReadFile } from '../util/safe_fs.js';
7
+
8
+ /**
9
+ * /agents 커맨드 - AGENTS.md 파일 조회
10
+ *
11
+ * 사용법:
12
+ * /agents - 로드된 AGENTS.md 파일 목록 및 요약 표시
13
+ * /agents <path> - 특정 AGENTS.md 파일 전체 내용 표시
14
+ */
15
+ export default {
16
+ name: 'agents',
17
+ description: 'Show AGENTS.md files and their contents',
18
+ usage: '/agents [file-path]',
19
+ handler: async (args, context) => {
20
+ if (!args || args.length === 0) {
21
+ // AGENTS.md 요약 표시
22
+ const summary = await formatAgentsMdSummary();
23
+ uiEvents.addSystemMessage(summary);
24
+ return;
25
+ }
26
+
27
+ // 특정 파일 내용 표시
28
+ const filePath = args[0];
29
+
30
+ // 숫자가 입력된 경우 인덱스로 처리
31
+ const index = parseInt(filePath, 10);
32
+ if (!isNaN(index) && index > 0) {
33
+ const agentsMdFiles = await discoverAllAgentsMd();
34
+ if (index > agentsMdFiles.length) {
35
+ uiEvents.addSystemMessage(`Invalid index: ${index}. Found ${agentsMdFiles.length} AGENTS.md file(s).`);
36
+ return;
37
+ }
38
+
39
+ const file = agentsMdFiles[index - 1];
40
+ const info = [
41
+ `AGENTS.md (${file.source})`,
42
+ `Path: ${file.path}`,
43
+ '',
44
+ '--- Content ---',
45
+ '',
46
+ file.content
47
+ ].join('\n');
48
+
49
+ uiEvents.addSystemMessage(info);
50
+ return;
51
+ }
52
+
53
+ // 경로가 입력된 경우 해당 파일 읽기
54
+ try {
55
+ const content = await safeReadFile(filePath, 'utf8');
56
+ const info = [
57
+ `AGENTS.md`,
58
+ `Path: ${filePath}`,
59
+ '',
60
+ '--- Content ---',
61
+ '',
62
+ content
63
+ ].join('\n');
64
+
65
+ uiEvents.addSystemMessage(info);
66
+ } catch (error) {
67
+ uiEvents.addSystemMessage(`Failed to read file: ${filePath}\n${error.message}`);
68
+ }
69
+ }
70
+ };
@@ -15,7 +15,7 @@ export default {
15
15
  uiEvents.addSystemMessage(
16
16
  `Please enter API key.\n\n` +
17
17
  `Usage:\n` +
18
- ` /apikey sk-proj-...`
18
+ ` /apikey <your-zai-api-key>`
19
19
  );
20
20
  return;
21
21
  }
@@ -0,0 +1,129 @@
1
+ import { listBackgroundProcesses, killBackgroundProcess, getBackgroundProcess } from '../system/background_process.js';
2
+ import { uiEvents } from '../system/ui_events.js';
3
+
4
+ /**
5
+ * /bg 커맨드 - 백그라운드 프로세스 관리
6
+ *
7
+ * 사용법:
8
+ * /bg - 프로세스 목록 표시
9
+ * /bg list - 프로세스 목록 표시
10
+ * /bg kill <id> - 프로세스 종료
11
+ * /bg killall - 모든 실행 중인 프로세스 종료
12
+ */
13
+ export default {
14
+ name: 'bg',
15
+ description: 'Manage background processes',
16
+ usage: '/bg [list|kill <id>|killall]',
17
+ handler: async (args, context) => {
18
+ const subcommand = args[0]?.toLowerCase() || 'list';
19
+
20
+ switch (subcommand) {
21
+ case 'list':
22
+ case 'ls': {
23
+ const processes = listBackgroundProcesses({ running: true });
24
+ if (processes.length === 0) {
25
+ uiEvents.addSystemMessage('No running background processes');
26
+ return;
27
+ }
28
+
29
+ const lines = ['Background Processes:'];
30
+ for (const proc of processes) {
31
+ const statusIcon = proc.status === 'running' ? '●' :
32
+ proc.status === 'completed' ? '✓' :
33
+ proc.status === 'failed' ? '✗' : '⊘';
34
+ const duration = formatDuration(proc.startedAt, proc.endedAt);
35
+ lines.push(` ${statusIcon} [${proc.id}] pid:${proc.pid} ${duration} - ${proc.command.substring(0, 50)}${proc.command.length > 50 ? '...' : ''}`);
36
+ }
37
+ uiEvents.addSystemMessage(lines.join('\n'));
38
+ break;
39
+ }
40
+
41
+ case 'kill': {
42
+ const id = args[1];
43
+ if (!id) {
44
+ uiEvents.addErrorMessage('Usage: /bg kill <process_id>');
45
+ return;
46
+ }
47
+
48
+ const proc = getBackgroundProcess(id);
49
+ if (!proc) {
50
+ uiEvents.addErrorMessage(`Process not found: ${id}`);
51
+ return;
52
+ }
53
+
54
+ if (proc.status !== 'running') {
55
+ uiEvents.addSystemMessage(`Process ${id} is already ${proc.status}`);
56
+ return;
57
+ }
58
+
59
+ const killed = killBackgroundProcess(id);
60
+ if (killed) {
61
+ uiEvents.addSystemMessage(`Killed process: ${id} (pid: ${proc.pid})`);
62
+ } else {
63
+ uiEvents.addErrorMessage(`Failed to kill process: ${id}`);
64
+ }
65
+ break;
66
+ }
67
+
68
+ case 'killall': {
69
+ const processes = listBackgroundProcesses({ running: true });
70
+ if (processes.length === 0) {
71
+ uiEvents.addSystemMessage('No running background processes');
72
+ return;
73
+ }
74
+
75
+ let killedCount = 0;
76
+ for (const proc of processes) {
77
+ if (killBackgroundProcess(proc.id)) {
78
+ killedCount++;
79
+ }
80
+ }
81
+ uiEvents.addSystemMessage(`Killed ${killedCount} background process(es)`);
82
+ break;
83
+ }
84
+
85
+ case 'show': {
86
+ const id = args[1];
87
+ if (!id) {
88
+ uiEvents.addErrorMessage('Usage: /bg show <process_id>');
89
+ return;
90
+ }
91
+
92
+ const proc = getBackgroundProcess(id);
93
+ if (!proc) {
94
+ uiEvents.addErrorMessage(`Process not found: ${id}`);
95
+ return;
96
+ }
97
+
98
+ const lines = [
99
+ `Process: ${proc.id}`,
100
+ ` PID: ${proc.pid}`,
101
+ ` Status: ${proc.status}`,
102
+ ` Command: ${proc.command}`,
103
+ ` Started: ${proc.startedAt.toISOString()}`,
104
+ proc.endedAt ? ` Ended: ${proc.endedAt.toISOString()}` : null,
105
+ proc.exitCode !== null ? ` Exit Code: ${proc.exitCode}` : null,
106
+ proc.stdout ? ` Stdout (last 200 chars): ${proc.stdout.slice(-200)}` : null,
107
+ proc.stderr ? ` Stderr (last 200 chars): ${proc.stderr.slice(-200)}` : null,
108
+ ].filter(Boolean);
109
+
110
+ uiEvents.addSystemMessage(lines.join('\n'));
111
+ break;
112
+ }
113
+
114
+ default:
115
+ uiEvents.addErrorMessage(`Unknown subcommand: ${subcommand}\nUsage: /bg [list|kill <id>|killall|show <id>]`);
116
+ }
117
+ }
118
+ };
119
+
120
+ function formatDuration(startedAt, endedAt) {
121
+ const end = endedAt ? new Date(endedAt) : new Date();
122
+ const start = new Date(startedAt);
123
+ const diffMs = end - start;
124
+
125
+ if (diffMs < 1000) return `${diffMs}ms`;
126
+ if (diffMs < 60000) return `${Math.round(diffMs / 1000)}s`;
127
+ if (diffMs < 3600000) return `${Math.round(diffMs / 60000)}m`;
128
+ return `${Math.round(diffMs / 3600000)}h`;
129
+ }
@@ -0,0 +1,51 @@
1
+ import { uiEvents } from '../system/ui_events.js';
2
+ import {
3
+ formatCustomCommandList,
4
+ findCustomCommandByName
5
+ } from '../system/custom_command_loader.js';
6
+
7
+ /**
8
+ * /commands 커맨드 - 커스텀 커맨드 목록 조회 및 정보 확인
9
+ *
10
+ * 사용법:
11
+ * /commands - 모든 커스텀 커맨드 목록 표시
12
+ * /commands <name> - 특정 커맨드 상세 정보 표시
13
+ */
14
+ export default {
15
+ name: 'commands',
16
+ description: 'List custom commands or show command details',
17
+ usage: '/commands [command-name]',
18
+ handler: async (args, context) => {
19
+ if (!args || args.length === 0) {
20
+ // 커맨드 목록 표시
21
+ const commandList = await formatCustomCommandList();
22
+ uiEvents.addSystemMessage(commandList);
23
+ return;
24
+ }
25
+
26
+ // 특정 커맨드 정보 표시
27
+ const commandName = args[0];
28
+ const command = await findCustomCommandByName(commandName);
29
+
30
+ if (!command) {
31
+ uiEvents.addSystemMessage(`Custom command not found: ${commandName}\n\nUse /commands to see available commands.`);
32
+ return;
33
+ }
34
+
35
+ const info = [
36
+ `Command: ${command.name}`,
37
+ `Source: ${command.source}`,
38
+ `Path: ${command.path}`,
39
+ command.argumentHint ? `Argument Hint: ${command.argumentHint}` : null,
40
+ command.disableModelInvocation ? 'Model Invocation: Disabled (manual only)' : null,
41
+ '',
42
+ 'Frontmatter:',
43
+ JSON.stringify(command.frontmatter, null, 2),
44
+ '',
45
+ 'Content Preview:',
46
+ command.content.substring(0, 500) + (command.content.length > 500 ? '...' : '')
47
+ ].filter(Boolean).join('\n');
48
+
49
+ uiEvents.addSystemMessage(info);
50
+ }
51
+ };
@@ -0,0 +1,52 @@
1
+ import { uiEvents } from '../system/ui_events.js';
2
+ import { loadSettings, saveSettings, SETTINGS_FILE } from '../util/config.js';
3
+
4
+ /**
5
+ * /debug 커맨드 - API 요청/응답 내용 표시 설정
6
+ *
7
+ * 사용법:
8
+ * /debug - 현재 설정 상태 표시
9
+ * /debug on - API payload 표시 켜기
10
+ * /debug off - API payload 표시 끄기
11
+ */
12
+ export default {
13
+ name: 'debug',
14
+ description: 'Toggle API request/response payload display',
15
+ usage: '/debug [on|off]',
16
+ handler: async (args, context) => {
17
+ const settings = await loadSettings();
18
+ const currentValue = settings?.SHOW_API_PAYLOAD === true;
19
+
20
+ if (!args || args.length === 0) {
21
+ // 현재 상태 표시
22
+ const status = currentValue ? 'ON (표시됨)' : 'OFF (숨김)';
23
+ const message = [
24
+ `API Payload Display: ${status}`,
25
+ '',
26
+ 'Usage:',
27
+ ' /debug - Show current status',
28
+ ' /debug on - Enable API payload display',
29
+ ' /debug off - Disable API payload display',
30
+ '',
31
+ `Settings file: ${SETTINGS_FILE}`
32
+ ].join('\n');
33
+
34
+ uiEvents.addSystemMessage(message);
35
+ return;
36
+ }
37
+
38
+ const action = args[0]?.toLowerCase();
39
+
40
+ if (action === 'on' || action === 'true' || action === '1') {
41
+ settings.SHOW_API_PAYLOAD = true;
42
+ await saveSettings(settings);
43
+ uiEvents.addSystemMessage('API Payload Display: ON\n이제 AI 요청/응답 내용이 화면에 표시됩니다.');
44
+ } else if (action === 'off' || action === 'false' || action === '0') {
45
+ settings.SHOW_API_PAYLOAD = false;
46
+ await saveSettings(settings);
47
+ uiEvents.addSystemMessage('API Payload Display: OFF\nAI 요청/응답 내용이 화면에 표시되지 않습니다.');
48
+ } else {
49
+ uiEvents.addSystemMessage(`Unknown option: ${action}\nUsage: /debug [on|off]`);
50
+ }
51
+ }
52
+ };
@@ -2,6 +2,8 @@ import React from 'react';
2
2
  import { uiEvents } from '../system/ui_events.js';
3
3
  import { renderInkComponent } from '../frontend/utils/renderInkComponent.js';
4
4
  import { HelpView } from '../frontend/components/HelpView.js';
5
+ import { discoverAllCustomCommands } from '../system/custom_command_loader.js';
6
+ import { discoverAllSkills } from '../system/skill_loader.js';
5
7
 
6
8
  /**
7
9
  * /help 커맨드 - 사용 가능한 커맨드 목록 표시
@@ -15,8 +17,16 @@ export default {
15
17
 
16
18
  const commands = commandRegistry.getCommands();
17
19
 
20
+ // 커스텀 커맨드 로드
21
+ const customCommands = await discoverAllCustomCommands();
22
+
23
+ // 스킬 로드
24
+ const skills = await discoverAllSkills();
25
+
18
26
  const component = React.createElement(HelpView, {
19
- commands
27
+ commands,
28
+ customCommands,
29
+ skills
20
30
  });
21
31
 
22
32
  const output = await renderInkComponent(component);