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.
- package/README.md +198 -88
- package/index.js +43 -9
- package/package.json +4 -4
- package/payload_viewer/out/404/index.html +1 -1
- package/payload_viewer/out/404.html +1 -1
- package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
- package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
- package/payload_viewer/out/index.html +1 -1
- package/payload_viewer/out/index.txt +3 -3
- package/payload_viewer/web_server.js +361 -0
- package/src/LLMClient/client.js +392 -16
- package/src/LLMClient/converters/responses-to-claude.js +67 -18
- package/src/LLMClient/converters/responses-to-zai.js +608 -0
- package/src/LLMClient/errors.js +30 -4
- package/src/LLMClient/index.js +5 -0
- package/src/ai_based/completion_judge.js +35 -4
- package/src/ai_based/orchestrator.js +146 -35
- package/src/commands/agents.js +70 -0
- package/src/commands/apikey.js +1 -1
- package/src/commands/commands.js +51 -0
- package/src/commands/debug.js +52 -0
- package/src/commands/help.js +11 -1
- package/src/commands/model.js +42 -7
- package/src/commands/reasoning_effort.js +2 -2
- package/src/commands/skills.js +46 -0
- package/src/config/ai_models.js +106 -6
- package/src/config/constants.js +71 -0
- package/src/frontend/App.js +8 -0
- package/src/frontend/components/AutocompleteMenu.js +7 -1
- package/src/frontend/components/CurrentModelView.js +2 -2
- package/src/frontend/components/HelpView.js +106 -2
- package/src/frontend/components/Input.js +33 -11
- package/src/frontend/components/ModelListView.js +1 -1
- package/src/frontend/components/SetupWizard.js +51 -8
- package/src/frontend/hooks/useFileCompletion.js +467 -0
- package/src/frontend/utils/toolUIFormatter.js +261 -0
- package/src/system/agents_loader.js +289 -0
- package/src/system/ai_request.js +175 -12
- package/src/system/command_parser.js +33 -3
- package/src/system/conversation_state.js +265 -0
- package/src/system/custom_command_loader.js +386 -0
- package/src/system/session.js +59 -35
- package/src/system/skill_loader.js +318 -0
- package/src/system/tool_approval.js +10 -0
- package/src/tools/file_reader.js +49 -9
- package/src/tools/glob.js +0 -3
- package/src/tools/ripgrep.js +5 -7
- package/src/tools/skill_tool.js +122 -0
- package/src/tools/web_downloader.js +0 -3
- package/src/util/clone.js +174 -0
- package/src/util/config.js +38 -2
- package/src/util/config_migration.js +174 -0
- package/src/util/file_reference_parser.js +132 -0
- package/src/util/path_validator.js +178 -0
- package/src/util/prompt_loader.js +68 -1
- package/src/util/safe_fs.js +43 -3
- package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { uiEvents } from '../system/ui_events.js';
|
|
2
|
+
import { formatSkillList, findSkillByName, loadSkillAsPrompt } from '../system/skill_loader.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* /skills 커맨드 - 스킬 목록 조회 및 스킬 정보 확인
|
|
6
|
+
*
|
|
7
|
+
* 사용법:
|
|
8
|
+
* /skills - 모든 스킬 목록 표시
|
|
9
|
+
* /skills <name> - 특정 스킬 상세 정보 표시
|
|
10
|
+
*/
|
|
11
|
+
export default {
|
|
12
|
+
name: 'skills',
|
|
13
|
+
description: 'List available skills or show skill details',
|
|
14
|
+
usage: '/skills [skill-name]',
|
|
15
|
+
handler: async (args, context) => {
|
|
16
|
+
if (!args || args.length === 0) {
|
|
17
|
+
// 스킬 목록 표시
|
|
18
|
+
const skillList = await formatSkillList();
|
|
19
|
+
uiEvents.addSystemMessage(skillList);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 특정 스킬 정보 표시
|
|
24
|
+
const skillName = args[0];
|
|
25
|
+
const skill = await findSkillByName(skillName);
|
|
26
|
+
|
|
27
|
+
if (!skill) {
|
|
28
|
+
uiEvents.addSystemMessage(`Skill not found: ${skillName}\n\nUse /skills to see available skills.`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const info = [
|
|
33
|
+
`Skill: ${skill.name}`,
|
|
34
|
+
`Source: ${skill.source}`,
|
|
35
|
+
`Path: ${skill.path}`,
|
|
36
|
+
'',
|
|
37
|
+
'Frontmatter:',
|
|
38
|
+
JSON.stringify(skill.frontmatter, null, 2),
|
|
39
|
+
'',
|
|
40
|
+
'Content Preview:',
|
|
41
|
+
skill.content.substring(0, 500) + (skill.content.length > 500 ? '...' : '')
|
|
42
|
+
].join('\n');
|
|
43
|
+
|
|
44
|
+
uiEvents.addSystemMessage(info);
|
|
45
|
+
}
|
|
46
|
+
};
|
package/src/config/ai_models.js
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* 새 모델 추가 시 이 파일만 수정하면 됩니다.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* 공식 지원 Provider 목록
|
|
10
|
+
* 런타임에서 이 목록에 포함된 provider의 모델만 노출됩니다.
|
|
11
|
+
* 다른 provider의 구현 코드는 유지되지만 사용자에게 노출되지 않습니다.
|
|
12
|
+
*/
|
|
13
|
+
export const ENABLED_PROVIDERS = ['zai'];
|
|
14
|
+
|
|
8
15
|
export const AI_MODELS = {
|
|
9
16
|
// ========================================
|
|
10
17
|
// Claude 시리즈
|
|
@@ -62,6 +69,49 @@ export const AI_MODELS = {
|
|
|
62
69
|
maxTokens: 4096,
|
|
63
70
|
},
|
|
64
71
|
|
|
72
|
+
// ========================================
|
|
73
|
+
// Z.AI GLM 시리즈 (Anthropic API 호환)
|
|
74
|
+
// - interleaved-thinking 지원
|
|
75
|
+
// - 프롬프트 캐싱 지원 (cache_read_input_tokens)
|
|
76
|
+
// - 서버 측 웹 검색 지원
|
|
77
|
+
// ========================================
|
|
78
|
+
'glm-4.7': {
|
|
79
|
+
provider: 'zai',
|
|
80
|
+
name: 'GLM-4.7',
|
|
81
|
+
contextWindow: 200000, // 200K
|
|
82
|
+
maxTokens: 128000, // 128K
|
|
83
|
+
supportsThinking: true,
|
|
84
|
+
supportsCaching: true,
|
|
85
|
+
supportsServerTools: true,
|
|
86
|
+
},
|
|
87
|
+
'glm-4.6': {
|
|
88
|
+
provider: 'zai',
|
|
89
|
+
name: 'GLM-4.6',
|
|
90
|
+
contextWindow: 200000, // 200K
|
|
91
|
+
maxTokens: 128000, // 128K
|
|
92
|
+
supportsThinking: true,
|
|
93
|
+
supportsCaching: true,
|
|
94
|
+
supportsServerTools: true,
|
|
95
|
+
},
|
|
96
|
+
'glm-4.5-air': {
|
|
97
|
+
provider: 'zai',
|
|
98
|
+
name: 'GLM-4.5 Air',
|
|
99
|
+
contextWindow: 128000, // 128K
|
|
100
|
+
maxTokens: 96000, // 96K
|
|
101
|
+
supportsThinking: true,
|
|
102
|
+
supportsCaching: true,
|
|
103
|
+
supportsServerTools: true,
|
|
104
|
+
},
|
|
105
|
+
'glm-4.5': {
|
|
106
|
+
provider: 'zai',
|
|
107
|
+
name: 'GLM-4.5',
|
|
108
|
+
contextWindow: 128000, // 128K
|
|
109
|
+
maxTokens: 96000, // 96K
|
|
110
|
+
supportsThinking: true,
|
|
111
|
+
supportsCaching: true,
|
|
112
|
+
supportsServerTools: true,
|
|
113
|
+
},
|
|
114
|
+
|
|
65
115
|
// ========================================
|
|
66
116
|
// Google Gemini 시리즈
|
|
67
117
|
// ========================================
|
|
@@ -137,6 +187,14 @@ export function getModelInfo(modelId) {
|
|
|
137
187
|
return AI_MODELS[modelId] || null;
|
|
138
188
|
}
|
|
139
189
|
|
|
190
|
+
/**
|
|
191
|
+
* 모델이 공식 지원 provider인지 확인
|
|
192
|
+
*/
|
|
193
|
+
export function isModelEnabled(modelId) {
|
|
194
|
+
const model = AI_MODELS[modelId];
|
|
195
|
+
return model ? ENABLED_PROVIDERS.includes(model.provider) : false;
|
|
196
|
+
}
|
|
197
|
+
|
|
140
198
|
/**
|
|
141
199
|
* 모델 ID로 max_tokens 가져오기
|
|
142
200
|
*/
|
|
@@ -154,16 +212,21 @@ export function getContextWindow(modelId) {
|
|
|
154
212
|
}
|
|
155
213
|
|
|
156
214
|
/**
|
|
157
|
-
* 모든 모델 ID 목록 가져오기
|
|
215
|
+
* 모든 모델 ID 목록 가져오기 (공식 지원 provider만)
|
|
158
216
|
*/
|
|
159
217
|
export function getAllModelIds() {
|
|
160
|
-
return Object.keys(AI_MODELS)
|
|
218
|
+
return Object.keys(AI_MODELS).filter(
|
|
219
|
+
modelId => ENABLED_PROVIDERS.includes(AI_MODELS[modelId].provider)
|
|
220
|
+
);
|
|
161
221
|
}
|
|
162
222
|
|
|
163
223
|
/**
|
|
164
|
-
* 특정 제조사의 모델 ID 목록 가져오기
|
|
224
|
+
* 특정 제조사의 모델 ID 목록 가져오기 (공식 지원 provider만)
|
|
165
225
|
*/
|
|
166
226
|
export function getModelsByProvider(provider) {
|
|
227
|
+
if (!ENABLED_PROVIDERS.includes(provider)) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
167
230
|
return Object.keys(AI_MODELS).filter(
|
|
168
231
|
modelId => AI_MODELS[modelId].provider === provider
|
|
169
232
|
);
|
|
@@ -177,14 +240,51 @@ export function getGPT5Models() {
|
|
|
177
240
|
}
|
|
178
241
|
|
|
179
242
|
/**
|
|
180
|
-
* Reasoning 지원 모델 ID 목록
|
|
243
|
+
* Reasoning 지원 모델 ID 목록 (공식 지원 provider만)
|
|
181
244
|
*/
|
|
182
245
|
export function getReasoningModels() {
|
|
183
246
|
return Object.keys(AI_MODELS).filter(
|
|
184
|
-
modelId => AI_MODELS[modelId].supportsReasoning
|
|
247
|
+
modelId => AI_MODELS[modelId].supportsReasoning &&
|
|
248
|
+
ENABLED_PROVIDERS.includes(AI_MODELS[modelId].provider)
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Thinking 지원 모델 ID 목록 (Z.AI GLM 등, 공식 지원 provider만)
|
|
254
|
+
*/
|
|
255
|
+
export function getThinkingModels() {
|
|
256
|
+
return Object.keys(AI_MODELS).filter(
|
|
257
|
+
modelId => AI_MODELS[modelId].supportsThinking &&
|
|
258
|
+
ENABLED_PROVIDERS.includes(AI_MODELS[modelId].provider)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 캐싱 지원 모델 ID 목록 (공식 지원 provider만)
|
|
264
|
+
*/
|
|
265
|
+
export function getCachingModels() {
|
|
266
|
+
return Object.keys(AI_MODELS).filter(
|
|
267
|
+
modelId => AI_MODELS[modelId].supportsCaching &&
|
|
268
|
+
ENABLED_PROVIDERS.includes(AI_MODELS[modelId].provider)
|
|
185
269
|
);
|
|
186
270
|
}
|
|
187
271
|
|
|
272
|
+
/**
|
|
273
|
+
* 특정 모델이 thinking을 지원하는지 확인
|
|
274
|
+
*/
|
|
275
|
+
export function supportsThinking(modelId) {
|
|
276
|
+
const model = AI_MODELS[modelId];
|
|
277
|
+
return model?.supportsThinking || false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 특정 모델이 캐싱을 지원하는지 확인
|
|
282
|
+
*/
|
|
283
|
+
export function supportsCaching(modelId) {
|
|
284
|
+
const model = AI_MODELS[modelId];
|
|
285
|
+
return model?.supportsCaching || false;
|
|
286
|
+
}
|
|
287
|
+
|
|
188
288
|
/**
|
|
189
289
|
* 특정 모델이 특정 reasoning effort를 지원하는지 확인
|
|
190
290
|
*/
|
|
@@ -199,7 +299,7 @@ export function supportsReasoningEffort(modelId, effort) {
|
|
|
199
299
|
/**
|
|
200
300
|
* 기본 권장 모델 ID
|
|
201
301
|
*/
|
|
202
|
-
export const DEFAULT_MODEL = '
|
|
302
|
+
export const DEFAULT_MODEL = 'glm-4.5';
|
|
203
303
|
|
|
204
304
|
// ========================================
|
|
205
305
|
// 하위 호환성을 위한 별칭 (Deprecated)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 전역 상수 정의
|
|
3
|
+
* 하드코딩된 값들을 중앙에서 관리합니다.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============================================
|
|
7
|
+
// Session 관련 상수
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
/** 서브미션 이름의 최대 길이 */
|
|
11
|
+
export const SUB_MISSION_MAX_LENGTH = 120;
|
|
12
|
+
|
|
13
|
+
/** 기본 최대 반복 횟수 */
|
|
14
|
+
export const DEFAULT_MAX_ITERATIONS = 50;
|
|
15
|
+
|
|
16
|
+
/** reasoning만 있는 응답의 최대 허용 횟수 */
|
|
17
|
+
export const MAX_REASONING_ONLY_RESPONSES = 5;
|
|
18
|
+
|
|
19
|
+
// ============================================
|
|
20
|
+
// Ripgrep 관련 상수
|
|
21
|
+
// ============================================
|
|
22
|
+
|
|
23
|
+
/** 기본 타임아웃 (ms) */
|
|
24
|
+
export const RIPGREP_DEFAULT_TIMEOUT_MS = 120000;
|
|
25
|
+
|
|
26
|
+
/** 기본 최대 매치 수 */
|
|
27
|
+
export const RIPGREP_DEFAULT_MAX_COUNT = 500;
|
|
28
|
+
|
|
29
|
+
/** 최대 출력 크기 (bytes) */
|
|
30
|
+
export const RIPGREP_MAX_OUTPUT_SIZE = 30000;
|
|
31
|
+
|
|
32
|
+
// ============================================
|
|
33
|
+
// File Reader 관련 상수
|
|
34
|
+
// ============================================
|
|
35
|
+
|
|
36
|
+
/** 파일 읽기 최대 줄 수 */
|
|
37
|
+
export const FILE_READER_MAX_LINES = 2000;
|
|
38
|
+
|
|
39
|
+
/** 파일 크기 제한 (bytes) - 10MB */
|
|
40
|
+
export const FILE_READER_MAX_SIZE_BYTES = 10 * 1024 * 1024;
|
|
41
|
+
|
|
42
|
+
// ============================================
|
|
43
|
+
// AI 요청 관련 상수
|
|
44
|
+
// ============================================
|
|
45
|
+
|
|
46
|
+
/** 컨텍스트 trim 후 재시도를 위한 메시지 최소 수 */
|
|
47
|
+
export const MIN_MESSAGES_FOR_RETRY = 3;
|
|
48
|
+
|
|
49
|
+
// ============================================
|
|
50
|
+
// UI/Output 관련 상수
|
|
51
|
+
// ============================================
|
|
52
|
+
|
|
53
|
+
/** 출력 클램핑 최대 길이 */
|
|
54
|
+
export const OUTPUT_CLAMP_MAX_LENGTH = 30000;
|
|
55
|
+
|
|
56
|
+
// ============================================
|
|
57
|
+
// MCP 관련 상수
|
|
58
|
+
// ============================================
|
|
59
|
+
|
|
60
|
+
/** MCP 서버 연결 타임아웃 (ms) */
|
|
61
|
+
export const MCP_CONNECTION_TIMEOUT_MS = 30000;
|
|
62
|
+
|
|
63
|
+
// ============================================
|
|
64
|
+
// 에러 재시도 관련 상수
|
|
65
|
+
// ============================================
|
|
66
|
+
|
|
67
|
+
/** 세션 저장 최대 재시도 횟수 */
|
|
68
|
+
export const SESSION_SAVE_MAX_RETRIES = 3;
|
|
69
|
+
|
|
70
|
+
/** 세션 저장 재시도 지연 (ms) */
|
|
71
|
+
export const SESSION_SAVE_RETRY_DELAY_MS = 1000;
|
package/src/frontend/App.js
CHANGED
|
@@ -888,6 +888,14 @@ export function App({ onSubmit, onClearScreen, onExit, commands = [], model, ver
|
|
|
888
888
|
if (event.todos) {
|
|
889
889
|
debugLog(`[handleTodosUpdate] Updating todos: ${event.todos.length} items`);
|
|
890
890
|
setTodos(event.todos);
|
|
891
|
+
|
|
892
|
+
// 모든 todo가 completed 상태면 잠시 후 자동으로 제거
|
|
893
|
+
const allCompleted = event.todos.length > 0 && event.todos.every(todo => todo.status === 'completed');
|
|
894
|
+
if (allCompleted) {
|
|
895
|
+
setTimeout(() => {
|
|
896
|
+
setTodos([]);
|
|
897
|
+
}, 1500);
|
|
898
|
+
}
|
|
891
899
|
}
|
|
892
900
|
};
|
|
893
901
|
|
|
@@ -21,13 +21,19 @@ export function AutocompleteMenu({ suggestions = [], activeIndex = 0 }) {
|
|
|
21
21
|
React.createElement(Text, { color: theme.text.secondary, bold: true }, 'Suggestions'),
|
|
22
22
|
suggestions.map((suggestion, index) => {
|
|
23
23
|
const isActive = index === activeIndex;
|
|
24
|
+
// 아이콘이 있으면 표시, 없으면 값만 표시
|
|
25
|
+
const icon = suggestion.icon || '';
|
|
26
|
+
const displayText = suggestion.displayValue || suggestion.value;
|
|
27
|
+
const prefix = icon ? `${icon} ` : '';
|
|
28
|
+
|
|
24
29
|
return React.createElement(Box, { key: index, marginLeft: 1 },
|
|
25
30
|
React.createElement(Text, {
|
|
26
31
|
color: isActive ? theme.text.accent : theme.text.primary,
|
|
27
32
|
bold: isActive
|
|
28
33
|
},
|
|
29
34
|
isActive ? '▶ ' : ' ',
|
|
30
|
-
|
|
35
|
+
prefix,
|
|
36
|
+
displayText,
|
|
31
37
|
suggestion.description && React.createElement(Text, { color: theme.text.secondary },
|
|
32
38
|
' - ' + suggestion.description)
|
|
33
39
|
)
|
|
@@ -8,11 +8,11 @@ import { theme } from '../design/themeColors.js';
|
|
|
8
8
|
|
|
9
9
|
export function CurrentModelView({ provider, modelId, modelInfo }) {
|
|
10
10
|
const providerColors = {
|
|
11
|
-
|
|
11
|
+
zai: 'cyan'
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
const providerEmojis = {
|
|
15
|
-
|
|
15
|
+
zai: '🤖'
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
return React.createElement(Box, {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
import { Box, Text } from 'ink';
|
|
7
7
|
|
|
8
|
-
export function HelpView({ commands }) {
|
|
8
|
+
export function HelpView({ commands, customCommands = [], skills = [] }) {
|
|
9
9
|
// 커맨드를 카테고리별로 분류
|
|
10
10
|
const aiCommands = commands.filter(cmd =>
|
|
11
11
|
['model', 'reasoning_effort', 'apikey'].includes(cmd.name)
|
|
@@ -59,6 +59,96 @@ export function HelpView({ commands }) {
|
|
|
59
59
|
);
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
// 커스텀 커맨드 렌더링
|
|
63
|
+
const renderCustomCommands = () => {
|
|
64
|
+
if (customCommands.length === 0) return null;
|
|
65
|
+
|
|
66
|
+
return React.createElement(Box, {
|
|
67
|
+
flexDirection: 'column',
|
|
68
|
+
borderStyle: 'round',
|
|
69
|
+
borderColor: 'gray',
|
|
70
|
+
paddingX: 2,
|
|
71
|
+
paddingY: 1,
|
|
72
|
+
marginBottom: 1
|
|
73
|
+
},
|
|
74
|
+
React.createElement(Text, {
|
|
75
|
+
bold: true,
|
|
76
|
+
color: 'cyan'
|
|
77
|
+
}, '📋 Custom Commands'),
|
|
78
|
+
React.createElement(Text, null),
|
|
79
|
+
|
|
80
|
+
customCommands.map(cmd =>
|
|
81
|
+
React.createElement(Box, {
|
|
82
|
+
key: cmd.name,
|
|
83
|
+
flexDirection: 'column',
|
|
84
|
+
marginBottom: 1
|
|
85
|
+
},
|
|
86
|
+
React.createElement(Box, { flexDirection: 'row', gap: 1 },
|
|
87
|
+
React.createElement(Text, { color: 'green', bold: true }, '•'),
|
|
88
|
+
React.createElement(Text, { color: 'white', bold: true },
|
|
89
|
+
`/${cmd.name}${cmd.argumentHint ? ' ' + cmd.argumentHint : ''}`
|
|
90
|
+
),
|
|
91
|
+
React.createElement(Text, { color: 'gray', dimColor: true },
|
|
92
|
+
` (${cmd.source})`
|
|
93
|
+
)
|
|
94
|
+
),
|
|
95
|
+
cmd.description && React.createElement(Box, {
|
|
96
|
+
flexDirection: 'row',
|
|
97
|
+
marginLeft: 2
|
|
98
|
+
},
|
|
99
|
+
React.createElement(Text, { color: 'gray' },
|
|
100
|
+
cmd.description.substring(0, 60) + (cmd.description.length > 60 ? '...' : '')
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// 스킬 렌더링
|
|
109
|
+
const renderSkills = () => {
|
|
110
|
+
if (skills.length === 0) return null;
|
|
111
|
+
|
|
112
|
+
return React.createElement(Box, {
|
|
113
|
+
flexDirection: 'column',
|
|
114
|
+
borderStyle: 'round',
|
|
115
|
+
borderColor: 'gray',
|
|
116
|
+
paddingX: 2,
|
|
117
|
+
paddingY: 1,
|
|
118
|
+
marginBottom: 1
|
|
119
|
+
},
|
|
120
|
+
React.createElement(Text, {
|
|
121
|
+
bold: true,
|
|
122
|
+
color: 'cyan'
|
|
123
|
+
}, '🎯 Skills'),
|
|
124
|
+
React.createElement(Text, null),
|
|
125
|
+
|
|
126
|
+
skills.map(skill =>
|
|
127
|
+
React.createElement(Box, {
|
|
128
|
+
key: skill.name,
|
|
129
|
+
flexDirection: 'column',
|
|
130
|
+
marginBottom: 1
|
|
131
|
+
},
|
|
132
|
+
React.createElement(Box, { flexDirection: 'row', gap: 1 },
|
|
133
|
+
React.createElement(Text, { color: 'green', bold: true }, '•'),
|
|
134
|
+
React.createElement(Text, { color: 'white', bold: true }, `/${skill.name}`),
|
|
135
|
+
React.createElement(Text, { color: 'gray', dimColor: true },
|
|
136
|
+
` (${skill.source})`
|
|
137
|
+
)
|
|
138
|
+
),
|
|
139
|
+
skill.description && React.createElement(Box, {
|
|
140
|
+
flexDirection: 'row',
|
|
141
|
+
marginLeft: 2
|
|
142
|
+
},
|
|
143
|
+
React.createElement(Text, { color: 'gray' },
|
|
144
|
+
skill.description.substring(0, 60) + (skill.description.length > 60 ? '...' : '')
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
62
152
|
return React.createElement(Box, {
|
|
63
153
|
flexDirection: 'column',
|
|
64
154
|
paddingX: 2,
|
|
@@ -89,6 +179,12 @@ export function HelpView({ commands }) {
|
|
|
89
179
|
// Other Commands
|
|
90
180
|
renderCommandGroup('🔧 Other', otherCommands),
|
|
91
181
|
|
|
182
|
+
// Custom Commands
|
|
183
|
+
renderCustomCommands(),
|
|
184
|
+
|
|
185
|
+
// Skills
|
|
186
|
+
renderSkills(),
|
|
187
|
+
|
|
92
188
|
// Footer
|
|
93
189
|
React.createElement(Box, {
|
|
94
190
|
flexDirection: 'column',
|
|
@@ -104,7 +200,15 @@ export function HelpView({ commands }) {
|
|
|
104
200
|
React.createElement(Text, {
|
|
105
201
|
dimColor: true,
|
|
106
202
|
italic: true
|
|
107
|
-
}, '💡 Press Ctrl+C to cancel current operation')
|
|
203
|
+
}, '💡 Press Ctrl+C to cancel current operation'),
|
|
204
|
+
React.createElement(Text, {
|
|
205
|
+
dimColor: true,
|
|
206
|
+
italic: true
|
|
207
|
+
}, '💡 Custom commands: ~/.aiexe/commands/ or .aiexe/commands/'),
|
|
208
|
+
React.createElement(Text, {
|
|
209
|
+
dimColor: true,
|
|
210
|
+
italic: true
|
|
211
|
+
}, '💡 Skills: ~/.aiexe/skills/ or .aiexe/skills/')
|
|
108
212
|
)
|
|
109
213
|
);
|
|
110
214
|
}
|
|
@@ -7,6 +7,7 @@ import { Box, Text } from 'ink';
|
|
|
7
7
|
import { theme } from '../design/themeColors.js';
|
|
8
8
|
import { useKeypress, keyMatchers, Command } from '../hooks/useKeypress.js';
|
|
9
9
|
import { useCompletion } from '../hooks/useCompletion.js';
|
|
10
|
+
import { useFileCompletion } from '../hooks/useFileCompletion.js';
|
|
10
11
|
import { AutocompleteMenu } from './AutocompleteMenu.js';
|
|
11
12
|
import { cpSlice, cpLen } from '../utils/inputBuffer.js';
|
|
12
13
|
import { uiEvents } from '../../system/ui_events.js';
|
|
@@ -48,18 +49,31 @@ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, command
|
|
|
48
49
|
// placeholder가 제공되지 않으면 무작위로 선택 (컴포넌트 마운트 시 한 번만)
|
|
49
50
|
const defaultPlaceholder = useMemo(() => placeholder || getRandomPlaceholder(), []);
|
|
50
51
|
|
|
51
|
-
const
|
|
52
|
+
const commandCompletionRaw = useCompletion(buffer, commands);
|
|
53
|
+
const fileCompletionRaw = useFileCompletion(buffer);
|
|
54
|
+
|
|
55
|
+
// Stabilize completion objects to prevent handleInput from being recreated
|
|
56
|
+
const commandCompletion = useMemo(() => commandCompletionRaw, [
|
|
57
|
+
commandCompletionRaw.showSuggestions,
|
|
58
|
+
commandCompletionRaw.suggestions.length,
|
|
59
|
+
commandCompletionRaw.activeSuggestionIndex,
|
|
60
|
+
commandCompletionRaw.handleAutocomplete,
|
|
61
|
+
commandCompletionRaw.navigateUp,
|
|
62
|
+
commandCompletionRaw.navigateDown
|
|
63
|
+
]);
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
completionRaw.navigateDown
|
|
65
|
+
const fileCompletion = useMemo(() => fileCompletionRaw, [
|
|
66
|
+
fileCompletionRaw.showSuggestions,
|
|
67
|
+
fileCompletionRaw.suggestions.length,
|
|
68
|
+
fileCompletionRaw.activeSuggestionIndex,
|
|
69
|
+
fileCompletionRaw.handleAutocomplete,
|
|
70
|
+
fileCompletionRaw.navigateUp,
|
|
71
|
+
fileCompletionRaw.navigateDown
|
|
61
72
|
]);
|
|
62
73
|
|
|
74
|
+
// 파일 자동완성 우선, 그 다음 명령어 자동완성
|
|
75
|
+
const completion = fileCompletion.showSuggestions ? fileCompletion : commandCompletion;
|
|
76
|
+
|
|
63
77
|
|
|
64
78
|
const handleSubmitAndClear = useCallback((submittedValue) => {
|
|
65
79
|
buffer.setText('');
|
|
@@ -139,9 +153,17 @@ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, command
|
|
|
139
153
|
// If suggestions are shown, accept the active suggestion
|
|
140
154
|
if (completion.showSuggestions && completion.suggestions.length > 0) {
|
|
141
155
|
const targetIndex = completion.activeSuggestionIndex === -1 ? 0 : completion.activeSuggestionIndex;
|
|
156
|
+
const suggestion = completion.suggestions[targetIndex];
|
|
157
|
+
|
|
158
|
+
// 파일 자동완성인 경우 (@로 시작) 완성만 하고 submit하지 않음
|
|
159
|
+
if (suggestion.value.startsWith('@')) {
|
|
160
|
+
completion.handleAutocomplete(targetIndex);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 명령어 자동완성인 경우 완성 후 바로 실행
|
|
142
165
|
completion.handleAutocomplete(targetIndex);
|
|
143
|
-
|
|
144
|
-
const completedCommand = completion.suggestions[targetIndex].value;
|
|
166
|
+
const completedCommand = suggestion.value;
|
|
145
167
|
handleSubmitAndClear(completedCommand);
|
|
146
168
|
return;
|
|
147
169
|
}
|
|
@@ -40,7 +40,7 @@ export function ModelListView({ modelsByProvider }) {
|
|
|
40
40
|
React.createElement(Text, { key: 'spacer2' }, null),
|
|
41
41
|
React.createElement(Text, { key: 'usage', bold: true }, 'Usage:'),
|
|
42
42
|
React.createElement(Text, { key: 'usage-cmd' }, ' /model <model-id>'),
|
|
43
|
-
React.createElement(Text, { key: 'usage-example', dimColor: true }, ' Example: /model
|
|
43
|
+
React.createElement(Text, { key: 'usage-example', dimColor: true }, ' Example: /model glm-4.5')
|
|
44
44
|
);
|
|
45
45
|
|
|
46
46
|
return React.createElement(Box, {
|
|
@@ -5,7 +5,22 @@
|
|
|
5
5
|
import React, { useState, useRef } from 'react';
|
|
6
6
|
import { Box, Text, useInput } from 'ink';
|
|
7
7
|
import { theme } from '../design/themeColors.js';
|
|
8
|
-
import { AI_MODELS, getAllModelIds, DEFAULT_MODEL } from '../../config/ai_models.js';
|
|
8
|
+
import { AI_MODELS, getAllModelIds, getModelsByProvider, DEFAULT_MODEL } from '../../config/ai_models.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* API 키 스타일로 provider 감지
|
|
12
|
+
* @param {string} apiKey - API 키
|
|
13
|
+
* @returns {string|null} provider 이름 또는 null
|
|
14
|
+
*/
|
|
15
|
+
function detectProviderFromApiKey(apiKey) {
|
|
16
|
+
if (!apiKey || typeof apiKey !== 'string') return null;
|
|
17
|
+
|
|
18
|
+
// Z.AI API 키 형식: 32자리 hex + '.' + 16자리 영숫자
|
|
19
|
+
if (/^[a-f0-9]{32}\.[A-Za-z0-9]{16}$/.test(apiKey)) {
|
|
20
|
+
return 'zai';
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
9
24
|
|
|
10
25
|
const STEPS = {
|
|
11
26
|
API_KEY: 'api_key',
|
|
@@ -17,6 +32,8 @@ export function SetupWizard({ onComplete, onCancel }) {
|
|
|
17
32
|
const [step, setStep] = useState(STEPS.API_KEY);
|
|
18
33
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
19
34
|
const [textInput, setTextInput] = useState('');
|
|
35
|
+
const [detectedProvider, setDetectedProvider] = useState(null);
|
|
36
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
20
37
|
|
|
21
38
|
// settings를 ref로 관리하여 stale closure 문제 방지
|
|
22
39
|
const settingsRef = useRef({
|
|
@@ -25,6 +42,14 @@ export function SetupWizard({ onComplete, onCancel }) {
|
|
|
25
42
|
REASONING_EFFORT: 'medium'
|
|
26
43
|
});
|
|
27
44
|
|
|
45
|
+
// 감지된 provider에 따른 모델 목록
|
|
46
|
+
const getAvailableModels = () => {
|
|
47
|
+
if (detectedProvider) {
|
|
48
|
+
return getModelsByProvider(detectedProvider);
|
|
49
|
+
}
|
|
50
|
+
return getAllModelIds();
|
|
51
|
+
};
|
|
52
|
+
|
|
28
53
|
// 현재 스텝이 텍스트 입력인지 선택지인지 판단
|
|
29
54
|
const isTextInputStep = step === STEPS.API_KEY;
|
|
30
55
|
|
|
@@ -41,14 +66,26 @@ export function SetupWizard({ onComplete, onCancel }) {
|
|
|
41
66
|
if (!textInput.trim()) {
|
|
42
67
|
return;
|
|
43
68
|
}
|
|
44
|
-
|
|
69
|
+
const apiKey = textInput.trim();
|
|
70
|
+
// API 키 스타일로 provider 감지
|
|
71
|
+
const provider = detectProviderFromApiKey(apiKey);
|
|
72
|
+
if (!provider) {
|
|
73
|
+
// 유효하지 않은 API 키 형식
|
|
74
|
+
setErrorMessage('Invalid API key format. Please use Z.AI API key.');
|
|
75
|
+
setTextInput('');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// 유효한 API 키
|
|
79
|
+
setErrorMessage('');
|
|
80
|
+
settingsRef.current.API_KEY = apiKey;
|
|
81
|
+
setDetectedProvider(provider);
|
|
45
82
|
setStep(STEPS.MODEL);
|
|
46
83
|
setTextInput('');
|
|
47
84
|
setSelectedIndex(0);
|
|
48
85
|
break;
|
|
49
86
|
|
|
50
87
|
case STEPS.MODEL:
|
|
51
|
-
const models =
|
|
88
|
+
const models = getAvailableModels();
|
|
52
89
|
const selectedModel = models[selectedIndex];
|
|
53
90
|
settingsRef.current.MODEL = selectedModel;
|
|
54
91
|
|
|
@@ -97,6 +134,10 @@ export function SetupWizard({ onComplete, onCancel }) {
|
|
|
97
134
|
|
|
98
135
|
if (input && !key.ctrl && !key.meta) {
|
|
99
136
|
setTextInput(prev => prev + input);
|
|
137
|
+
// 입력 시 에러 메시지 클리어
|
|
138
|
+
if (errorMessage) {
|
|
139
|
+
setErrorMessage('');
|
|
140
|
+
}
|
|
100
141
|
}
|
|
101
142
|
return;
|
|
102
143
|
}
|
|
@@ -118,7 +159,7 @@ export function SetupWizard({ onComplete, onCancel }) {
|
|
|
118
159
|
const getMaxIndexForStep = (currentStep) => {
|
|
119
160
|
switch (currentStep) {
|
|
120
161
|
case STEPS.MODEL:
|
|
121
|
-
return
|
|
162
|
+
return getAvailableModels().length - 1;
|
|
122
163
|
case STEPS.REASONING_EFFORT:
|
|
123
164
|
return 3; // 4 options
|
|
124
165
|
default:
|
|
@@ -145,16 +186,18 @@ export function SetupWizard({ onComplete, onCancel }) {
|
|
|
145
186
|
case STEPS.API_KEY:
|
|
146
187
|
return React.createElement(Box, { flexDirection: 'column' },
|
|
147
188
|
React.createElement(Text, { bold: true, color: theme.text.accent }, '1. API Key:'),
|
|
148
|
-
React.createElement(Text, { color: theme.text.secondary }, ' Get your API key from: https://
|
|
189
|
+
React.createElement(Text, { color: theme.text.secondary }, ' Get your API key from: https://z.ai/manage-apikey/apikey-list'),
|
|
149
190
|
React.createElement(Text, null),
|
|
150
191
|
React.createElement(Box, {
|
|
151
192
|
borderStyle: 'round',
|
|
152
|
-
borderColor: theme.border.focused,
|
|
193
|
+
borderColor: errorMessage ? theme.status.error : theme.border.focused,
|
|
153
194
|
paddingX: 1
|
|
154
195
|
},
|
|
155
196
|
React.createElement(Text, null, textInput ? '*'.repeat(textInput.length) : ' ')
|
|
156
197
|
),
|
|
157
|
-
|
|
198
|
+
errorMessage
|
|
199
|
+
? React.createElement(Text, { color: theme.status.error }, errorMessage)
|
|
200
|
+
: React.createElement(Text, null),
|
|
158
201
|
React.createElement(Text, { dimColor: true }, 'Type your API key and press Enter')
|
|
159
202
|
);
|
|
160
203
|
|
|
@@ -163,7 +206,7 @@ export function SetupWizard({ onComplete, onCancel }) {
|
|
|
163
206
|
React.createElement(Text, { bold: true, color: theme.text.accent }, '2. Choose Model:'),
|
|
164
207
|
React.createElement(Text, null),
|
|
165
208
|
renderOptions(
|
|
166
|
-
|
|
209
|
+
getAvailableModels().map(modelId => {
|
|
167
210
|
const model = AI_MODELS[modelId];
|
|
168
211
|
return `${modelId} (${model.name})`;
|
|
169
212
|
})
|