aiexecode 1.0.94 → 1.0.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aiexecode might be problematic. Click here for more details.

Files changed (60) hide show
  1. package/README.md +198 -88
  2. package/index.js +43 -9
  3. package/package.json +4 -4
  4. package/payload_viewer/out/404/index.html +1 -1
  5. package/payload_viewer/out/404.html +1 -1
  6. package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
  7. package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
  8. package/payload_viewer/out/index.html +1 -1
  9. package/payload_viewer/out/index.txt +3 -3
  10. package/payload_viewer/web_server.js +361 -0
  11. package/src/LLMClient/client.js +392 -16
  12. package/src/LLMClient/converters/responses-to-claude.js +67 -18
  13. package/src/LLMClient/converters/responses-to-zai.js +608 -0
  14. package/src/LLMClient/errors.js +30 -4
  15. package/src/LLMClient/index.js +5 -0
  16. package/src/ai_based/completion_judge.js +35 -4
  17. package/src/ai_based/orchestrator.js +146 -35
  18. package/src/commands/agents.js +70 -0
  19. package/src/commands/apikey.js +1 -1
  20. package/src/commands/commands.js +51 -0
  21. package/src/commands/debug.js +52 -0
  22. package/src/commands/help.js +11 -1
  23. package/src/commands/model.js +42 -7
  24. package/src/commands/reasoning_effort.js +2 -2
  25. package/src/commands/skills.js +46 -0
  26. package/src/config/ai_models.js +106 -6
  27. package/src/config/constants.js +71 -0
  28. package/src/frontend/App.js +8 -0
  29. package/src/frontend/components/AutocompleteMenu.js +7 -1
  30. package/src/frontend/components/CurrentModelView.js +2 -2
  31. package/src/frontend/components/HelpView.js +106 -2
  32. package/src/frontend/components/Input.js +33 -11
  33. package/src/frontend/components/ModelListView.js +1 -1
  34. package/src/frontend/components/SetupWizard.js +51 -8
  35. package/src/frontend/hooks/useFileCompletion.js +467 -0
  36. package/src/frontend/utils/toolUIFormatter.js +261 -0
  37. package/src/system/agents_loader.js +289 -0
  38. package/src/system/ai_request.js +175 -12
  39. package/src/system/command_parser.js +33 -3
  40. package/src/system/conversation_state.js +265 -0
  41. package/src/system/custom_command_loader.js +386 -0
  42. package/src/system/session.js +59 -35
  43. package/src/system/skill_loader.js +318 -0
  44. package/src/system/tool_approval.js +10 -0
  45. package/src/tools/file_reader.js +49 -9
  46. package/src/tools/glob.js +0 -3
  47. package/src/tools/ripgrep.js +5 -7
  48. package/src/tools/skill_tool.js +122 -0
  49. package/src/tools/web_downloader.js +0 -3
  50. package/src/util/clone.js +174 -0
  51. package/src/util/config.js +38 -2
  52. package/src/util/config_migration.js +174 -0
  53. package/src/util/file_reference_parser.js +132 -0
  54. package/src/util/path_validator.js +178 -0
  55. package/src/util/prompt_loader.js +68 -1
  56. package/src/util/safe_fs.js +43 -3
  57. package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
  58. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_buildManifest.js +0 -0
  59. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_clientMiddlewareManifest.json +0 -0
  60. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_ssgManifest.js +0 -0
@@ -182,15 +182,18 @@ async function dispatchCompletionJudgeRequest(options) {
182
182
  const estimatedTokens = Math.ceil(conversationJson.length / 4);
183
183
  debugLog(`[dispatchCompletionJudgeRequest] Estimated tokens: ${estimatedTokens} (conversation size: ${conversationJson.length} bytes)`);
184
184
 
185
- debugLog(`[dispatchCompletionJudgeRequest] Sending API request...`);
185
+ const apiStartTime = Date.now();
186
+ debugLog(`[dispatchCompletionJudgeRequest] >>>>> Sending API request at ${new Date(apiStartTime).toISOString()}...`);
186
187
  try {
187
188
  const response = await request(taskName, requestPayload);
188
- debugLog(`[dispatchCompletionJudgeRequest] API request successful`);
189
+ const apiDuration = Date.now() - apiStartTime;
190
+ debugLog(`[dispatchCompletionJudgeRequest] <<<<< API request successful in ${apiDuration}ms`);
189
191
  debugLog(`[dispatchCompletionJudgeRequest] Response output_text length: ${response?.output_text?.length || 0}`);
190
192
  debugLog(`[dispatchCompletionJudgeRequest] END - success`);
191
193
  return response;
192
194
  } catch (error) {
193
- debugLog(`[dispatchCompletionJudgeRequest] API request failed: ${error.message}`);
195
+ const apiDuration = Date.now() - apiStartTime;
196
+ debugLog(`[dispatchCompletionJudgeRequest] <<<<< API request FAILED after ${apiDuration}ms: ${error.message}`);
194
197
  debugLog(`[dispatchCompletionJudgeRequest] Error name: ${error.name}, type: ${error?.constructor?.name}`);
195
198
 
196
199
  // AbortError는 즉시 전파 (세션 중단)
@@ -288,7 +291,34 @@ export async function judgeMissionCompletion(templateVars = {}) {
288
291
  resetCompletionJudgeConversation();
289
292
 
290
293
  try {
291
- const judgment = JSON.parse(response.output_text);
294
+ let outputText = response.output_text || '';
295
+ let judgment = null;
296
+
297
+ // 1차 시도: 전체 텍스트를 JSON으로 파싱
298
+ try {
299
+ judgment = JSON.parse(outputText);
300
+ debugLog(`[judgeMissionCompletion] Parsed as pure JSON`);
301
+ } catch (e) {
302
+ // 2차 시도: 텍스트 끝에 붙은 JSON 추출 (GLM 모델 대응)
303
+ // 패턴: ...텍스트...{"should_complete":true,"whatUserShouldSay":"..."}
304
+ const jsonMatch = outputText.match(/\{[^{}]*"should_complete"\s*:\s*(true|false)[^{}]*\}/);
305
+ if (jsonMatch) {
306
+ debugLog(`[judgeMissionCompletion] Extracted JSON from text: ${jsonMatch[0]}`);
307
+ judgment = JSON.parse(jsonMatch[0]);
308
+ } else {
309
+ // 3차 시도: 마지막 { } 블록 추출
310
+ const lastBraceStart = outputText.lastIndexOf('{');
311
+ const lastBraceEnd = outputText.lastIndexOf('}');
312
+ if (lastBraceStart !== -1 && lastBraceEnd > lastBraceStart) {
313
+ const jsonCandidate = outputText.substring(lastBraceStart, lastBraceEnd + 1);
314
+ debugLog(`[judgeMissionCompletion] Trying last JSON block: ${jsonCandidate}`);
315
+ judgment = JSON.parse(jsonCandidate);
316
+ } else {
317
+ throw new Error('No JSON found in output');
318
+ }
319
+ }
320
+ }
321
+
292
322
  debugLog(`[judgeMissionCompletion] Parsed judgment successfully`);
293
323
  debugLog(`[judgeMissionCompletion] should_complete: ${judgment.should_complete}`);
294
324
  debugLog(`[judgeMissionCompletion] whatUserShouldSay: ${judgment.whatUserShouldSay}`);
@@ -308,6 +338,7 @@ export async function judgeMissionCompletion(templateVars = {}) {
308
338
  } catch (parseError) {
309
339
  debugLog(`[judgeMissionCompletion] Parse error: ${parseError.message}`);
310
340
  debugLog(`[judgeMissionCompletion] Failed to parse output_text as JSON`);
341
+ debugLog(`[judgeMissionCompletion] Raw output_text: ${response.output_text}`);
311
342
  throw new Error('Completion judge response did not include valid JSON output_text.');
312
343
  }
313
344
 
@@ -2,8 +2,9 @@ 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 } from "../util/prompt_loader.js";
6
6
  import { request, shouldRetryWithTrim, getModelForProvider } from "../system/ai_request.js";
7
+ import { supportsCaching } from "../config/ai_models.js";
7
8
  import { cleanupOrphanOutputs, trimConversation } 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";
@@ -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,120 @@ 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];
55
70
 
56
- const systemMessageEntry = {
57
- role: "system",
58
- content: [
59
- {
71
+ // AGENTS.md를 별도 블록으로 추가 (프로젝트별 지침)
72
+ if (agentsMdText) {
73
+ contentBlocks.push({
60
74
  type: "input_text",
61
- text: systemMessageText
62
- }
63
- ]
64
- };
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
+ }
90
+
91
+ // TODO 리마인더를 별도 블록으로 추가 (캐시 대상 아님)
92
+ if (currentTodos && currentTodos.length > 0) {
93
+ const todoReminder = createTodoReminder(currentTodos);
94
+ contentBlocks.push({
95
+ type: "input_text",
96
+ text: todoReminder
97
+ // cache_control 없음 - 동적 데이터이므로
98
+ });
99
+ debugLog(`[ensureConversationInitialized] Added ${currentTodos.length} todos as separate block (uncached)`);
100
+ }
101
+
102
+ systemMessageEntry = {
103
+ role: "system",
104
+ content: contentBlocks
105
+ };
106
+ } else {
107
+ // 일반 모델: 기존 방식 유지
108
+ let systemMessageText = systemMessage.content;
109
+
110
+ // AGENTS.md 추가 (프로젝트별 지침)
111
+ if (agentsMdText) {
112
+ systemMessageText += agentsMdText;
113
+ debugLog(`[ensureConversationInitialized] Added AGENTS.md to system message`);
114
+ }
115
+
116
+ // 스킬 목록 추가
117
+ if (skillListText) {
118
+ systemMessageText += skillListText;
119
+ debugLog(`[ensureConversationInitialized] Added skill list to system message`);
120
+ }
121
+
122
+ if (currentTodos && currentTodos.length > 0) {
123
+ const todosSection = '\n\n## Current Task List (Your TODO tracker)\n\n' +
124
+ 'You have created the following task list to track your work. Continue working on these tasks systematically:\n\n' +
125
+ currentTodos.map((todo, index) => {
126
+ const statusIcon = todo.status === 'completed' ? '✓' :
127
+ todo.status === 'in_progress' ? '→' : '○';
128
+ return `${index + 1}. [${statusIcon}] ${todo.content} (${todo.status})`;
129
+ }).join('\n') +
130
+ '\n\n**Remember**: Mark tasks as completed immediately after finishing them. Keep exactly ONE task as in_progress at a time.\n';
131
+
132
+ systemMessageText += todosSection;
133
+ debugLog(`[ensureConversationInitialized] Added ${currentTodos.length} todos to system message`);
134
+ }
135
+
136
+ systemMessageEntry = {
137
+ role: "system",
138
+ content: [
139
+ {
140
+ type: "input_text",
141
+ text: systemMessageText
142
+ }
143
+ ]
144
+ };
145
+ }
65
146
 
66
147
  // conversation이 비어있으면 새로 추가
67
148
  if (!orchestratorConversation.length) {
@@ -201,11 +282,13 @@ async function dispatchOrchestratorRequest({ toolChoice }) {
201
282
  const estimatedTokens = Math.ceil(conversationJson.length / 4); // 대략 4 chars = 1 token
202
283
  debugLog(`[dispatchOrchestratorRequest] Estimated tokens: ${estimatedTokens} (conversation size: ${conversationJson.length} bytes)`);
203
284
 
204
- debugLog(`[dispatchOrchestratorRequest] Sending API request...`);
285
+ const apiStartTime = Date.now();
286
+ debugLog(`[dispatchOrchestratorRequest] >>>>> Sending API request at ${new Date(apiStartTime).toISOString()}...`);
205
287
 
206
288
  try {
207
289
  const response = await request(taskName, requestConfig);
208
- debugLog(`[dispatchOrchestratorRequest] API request successful`);
290
+ const apiDuration = Date.now() - apiStartTime;
291
+ debugLog(`[dispatchOrchestratorRequest] <<<<< API request successful in ${apiDuration}ms`);
209
292
  debugLog(`[dispatchOrchestratorRequest] Response outputs: ${response?.output?.length || 0}`);
210
293
  if (response?.output) {
211
294
  const outputTypes = {};
@@ -219,7 +302,8 @@ async function dispatchOrchestratorRequest({ toolChoice }) {
219
302
  // consolelog(JSON.stringify(response, null, 2));
220
303
  return response;
221
304
  } catch (error) {
222
- debugLog(`[dispatchOrchestratorRequest] API request failed: ${error.message}`);
305
+ const apiDuration = Date.now() - apiStartTime;
306
+ debugLog(`[dispatchOrchestratorRequest] <<<<< API request FAILED after ${apiDuration}ms: ${error.message}`);
223
307
  debugLog(`[dispatchOrchestratorRequest] Error name: ${error.name}, type: ${error?.constructor?.name}`);
224
308
 
225
309
  // AbortError는 즉시 전파 (세션 중단)
@@ -308,7 +392,8 @@ export async function orchestrateMission({ improvement_points = '', mcpToolSchem
308
392
  response_message: responseMessageSchema,
309
393
  todo_write: todoWriteSchema,
310
394
  ripgrep: ripgrepSchema,
311
- glob_search: globSearchSchema
395
+ glob_search: globSearchSchema,
396
+ invoke_skill: skillSchema // 스킬 호출 도구
312
397
  };
313
398
 
314
399
  // Python 관련 도구 (조건부 추가)
@@ -355,14 +440,40 @@ export async function orchestrateMission({ improvement_points = '', mcpToolSchem
355
440
  debugLog(`[orchestrateMission] Conversation initialized, current length: ${orchestratorConversation.length}`);
356
441
 
357
442
  debugLog(`[orchestrateMission] Adding user message to conversation: "${improvementPointsText.substring(0, 100)}"`);
443
+
444
+ // 캐싱 모델인 경우 사용자 메시지에도 캐시 제어 적용 (Claude Code 스타일)
445
+ const useCache = supportsCaching(model);
446
+ const userContentBlocks = [];
447
+
448
+ // Auto-generated 메시지의 경우 system-reminder 추가
449
+ if (isAutoGenerated) {
450
+ const autoGenReminder = createSystemReminder(
451
+ '이 메시지는 시스템에서 자동 생성되었습니다. 사용자가 직접 입력한 것이 아닙니다.',
452
+ { hideFromUser: true }
453
+ );
454
+ userContentBlocks.push({
455
+ type: "input_text",
456
+ text: autoGenReminder
457
+ });
458
+ }
459
+
460
+ // 메인 사용자 메시지
461
+ const mainContent = {
462
+ type: "input_text",
463
+ text: improvementPointsText
464
+ };
465
+
466
+ // 캐싱 모델의 경우 사용자 메시지에도 캐시 적용 (반복되는 긴 메시지에 효과적)
467
+ if (useCache && improvementPointsText.length > 500) {
468
+ mainContent.cache_control = { type: "ephemeral" };
469
+ debugLog(`[orchestrateMission] Applied cache control to user message (${improvementPointsText.length} chars)`);
470
+ }
471
+
472
+ userContentBlocks.push(mainContent);
473
+
358
474
  const userMessage = {
359
475
  role: "user",
360
- content: [
361
- {
362
- type: "input_text",
363
- text: improvementPointsText
364
- }
365
- ]
476
+ content: userContentBlocks
366
477
  };
367
478
 
368
479
  // 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,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);
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
2
  import { uiEvents } from '../system/ui_events.js';
3
- import { loadSettings, saveSettings, SETTINGS_FILE } from '../util/config.js';
3
+ import { loadSettings, saveSettings, SETTINGS_FILE, getDefaultBaseUrlForProvider } from '../util/config.js';
4
4
  import { resetAIClients } from '../system/ai_request.js';
5
- import { AI_MODELS, getModelsByProvider, DEFAULT_MODEL } from '../config/ai_models.js';
5
+ import { AI_MODELS, getModelsByProvider, DEFAULT_MODEL, ENABLED_PROVIDERS, isModelEnabled } from '../config/ai_models.js';
6
6
  import { renderInkComponent } from '../frontend/utils/renderInkComponent.js';
7
7
  import { ModelListView } from '../frontend/components/ModelListView.js';
8
8
  import { CurrentModelView } from '../frontend/components/CurrentModelView.js';
@@ -14,10 +14,10 @@ function getProviderForModel(modelId) {
14
14
  return modelInfo ? modelInfo.provider : null;
15
15
  }
16
16
 
17
- // 모든 모델 목록 표시
17
+ // 모든 모델 목록 표시 (공식 지원 provider만)
18
18
  async function listAllModels() {
19
- // 모든 provider 목록 추출
20
- const providers = [...new Set(Object.values(AI_MODELS).map(m => m.provider))];
19
+ // 공식 지원 provider 사용
20
+ const providers = ENABLED_PROVIDERS;
21
21
 
22
22
  // provider별로 모델 그룹화
23
23
  const modelsByProvider = {};
@@ -63,7 +63,7 @@ async function showCurrentModel() {
63
63
  */
64
64
  export default {
65
65
  name: 'model',
66
- description: 'Select AI model (OpenAI)',
66
+ description: 'Select AI model',
67
67
  usage: '/model [model-id] or /model list',
68
68
  handler: async (args, context) => {
69
69
  // 인자가 없으면 현재 모델 표시
@@ -94,6 +94,16 @@ export default {
94
94
  return;
95
95
  }
96
96
 
97
+ // 공식 지원 provider 확인
98
+ if (!isModelEnabled(modelId)) {
99
+ uiEvents.addSystemMessage(
100
+ `Model not available: ${modelId}\n\n` +
101
+ `This model is not officially supported.\n` +
102
+ `Use \`/model list\` to see available models.`
103
+ );
104
+ return;
105
+ }
106
+
97
107
  try {
98
108
  // 현재 설정 로드
99
109
  const settings = await loadSettings();
@@ -102,6 +112,20 @@ export default {
102
112
  settings.MODEL = modelId;
103
113
  process.env.MODEL = modelId;
104
114
 
115
+ // Provider별 BASE_URL 자동 설정
116
+ const defaultBaseUrl = getDefaultBaseUrlForProvider(provider);
117
+ if (defaultBaseUrl) {
118
+ // Z.AI 등 특정 provider는 BASE_URL 자동 설정
119
+ settings.BASE_URL = defaultBaseUrl;
120
+ process.env.BASE_URL = defaultBaseUrl;
121
+ } else if (provider !== 'zai') {
122
+ // 다른 provider로 변경 시 BASE_URL 초기화 (Z.AI에서 다른 모델로 변경)
123
+ if (settings.BASE_URL === 'https://api.z.ai/api/anthropic') {
124
+ settings.BASE_URL = '';
125
+ delete process.env.BASE_URL;
126
+ }
127
+ }
128
+
105
129
  // 설정 저장
106
130
  await saveSettings(settings);
107
131
 
@@ -114,11 +138,22 @@ export default {
114
138
  // 성공 메시지
115
139
  const modelInfo = AI_MODELS[modelId];
116
140
 
141
+ // Provider별 경고 메시지 구성
117
142
  let warning = null;
118
143
  if (!settings.API_KEY) {
144
+ // Provider별 API 키 안내 (공식 지원 provider만)
145
+ const apiKeyHints = {
146
+ 'zai': 'Set your Z.AI API key with: `/apikey <your-key>`'
147
+ };
119
148
  warning = {
120
149
  message: 'API key is not configured.',
121
- hint: 'Set your API key with: `/apikey sk-proj-...`'
150
+ hint: apiKeyHints[provider] || 'Set your API key with: `/apikey <your-key>`'
151
+ };
152
+ } else if (provider === 'zai') {
153
+ // Z.AI 모델 선택 시 추가 안내
154
+ warning = {
155
+ message: `BASE_URL automatically set to: ${defaultBaseUrl}`,
156
+ hint: 'Ensure your API key is valid for Z.AI service.'
122
157
  };
123
158
  }
124
159
 
@@ -105,11 +105,11 @@ function listEffortLevels(currentModel) {
105
105
  }
106
106
 
107
107
  /**
108
- * /reasoning_effort 커맨드 - OpenAI reasoning_effort 설정
108
+ * /reasoning_effort 커맨드 - reasoning_effort 설정
109
109
  */
110
110
  export default {
111
111
  name: 'reasoning_effort',
112
- description: 'Set reasoning effort for OpenAI models (minimal, low, medium, high)',
112
+ description: 'Set reasoning effort for supported models (minimal, low, medium, high)',
113
113
  usage: '/reasoning_effort [level] or /reasoning_effort list',
114
114
  handler: async (args, context) => {
115
115
  // 모델 지원 확인