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.
Potentially problematic release.
This version of aiexecode might be problematic. Click here for more details.
- package/README.md +198 -88
- package/index.js +310 -86
- package/mcp-agent-lib/src/mcp_message_logger.js +17 -16
- 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/prompts/completion_judge.txt +4 -0
- package/prompts/orchestrator.txt +116 -3
- package/src/LLMClient/client.js +401 -18
- package/src/LLMClient/converters/responses-to-claude.js +67 -18
- package/src/LLMClient/converters/responses-to-zai.js +667 -0
- package/src/LLMClient/errors.js +30 -4
- package/src/LLMClient/index.js +5 -0
- package/src/ai_based/completion_judge.js +263 -186
- package/src/ai_based/orchestrator.js +171 -35
- package/src/commands/agents.js +70 -0
- package/src/commands/apikey.js +1 -1
- package/src/commands/bg.js +129 -0
- 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/config/feature_flags.js +6 -7
- package/src/frontend/App.js +108 -1
- package/src/frontend/components/AutocompleteMenu.js +7 -1
- package/src/frontend/components/BackgroundProcessList.js +175 -0
- package/src/frontend/components/ConversationItem.js +26 -10
- 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 +156 -12
- package/src/system/background_process.js +317 -0
- package/src/system/code_executer.js +496 -56
- package/src/system/command_parser.js +33 -3
- package/src/system/conversation_state.js +265 -0
- package/src/system/conversation_trimmer.js +132 -0
- package/src/system/custom_command_loader.js +386 -0
- package/src/system/file_integrity.js +73 -10
- package/src/system/log.js +10 -2
- package/src/system/output_helper.js +52 -9
- package/src/system/session.js +213 -58
- package/src/system/session_memory.js +30 -2
- package/src/system/skill_loader.js +318 -0
- package/src/system/system_info.js +254 -40
- package/src/system/tool_approval.js +10 -0
- package/src/system/tool_registry.js +15 -1
- package/src/system/ui_events.js +11 -0
- package/src/tools/code_editor.js +16 -10
- package/src/tools/file_reader.js +66 -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 +55 -2
- package/src/util/config_migration.js +174 -0
- package/src/util/debug_log.js +8 -2
- package/src/util/exit_handler.js +8 -0
- package/src/util/file_reference_parser.js +132 -0
- package/src/util/path_validator.js +178 -0
- package/src/util/prompt_loader.js +91 -1
- package/src/util/safe_fs.js +66 -3
- package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BackgroundProcessList Component
|
|
3
|
+
* 백그라운드 프로세스 목록을 표시하고 관리하는 컴포넌트
|
|
4
|
+
*
|
|
5
|
+
* - 방향키로 선택
|
|
6
|
+
* - k 키로 선택된 프로세스 종료
|
|
7
|
+
* - q 키로 목록 닫기
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useState, useEffect } from 'react';
|
|
11
|
+
import { Box, Text, useInput } from 'ink';
|
|
12
|
+
import { theme } from '../design/themeColors.js';
|
|
13
|
+
|
|
14
|
+
const getStatusIcon = (status) => {
|
|
15
|
+
switch (status) {
|
|
16
|
+
case 'running':
|
|
17
|
+
return '●';
|
|
18
|
+
case 'completed':
|
|
19
|
+
return '✓';
|
|
20
|
+
case 'failed':
|
|
21
|
+
return '✗';
|
|
22
|
+
case 'killed':
|
|
23
|
+
return '⊘';
|
|
24
|
+
default:
|
|
25
|
+
return '?';
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const getStatusColor = (status) => {
|
|
30
|
+
switch (status) {
|
|
31
|
+
case 'running':
|
|
32
|
+
return theme.brand.light;
|
|
33
|
+
case 'completed':
|
|
34
|
+
return 'green';
|
|
35
|
+
case 'failed':
|
|
36
|
+
return theme.status.error;
|
|
37
|
+
case 'killed':
|
|
38
|
+
return theme.text.secondary;
|
|
39
|
+
default:
|
|
40
|
+
return theme.text.primary;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const formatDuration = (startedAt, endedAt) => {
|
|
45
|
+
const end = endedAt ? new Date(endedAt) : new Date();
|
|
46
|
+
const start = new Date(startedAt);
|
|
47
|
+
const diffMs = end - start;
|
|
48
|
+
|
|
49
|
+
if (diffMs < 1000) return `${diffMs}ms`;
|
|
50
|
+
if (diffMs < 60000) return `${Math.round(diffMs / 1000)}s`;
|
|
51
|
+
if (diffMs < 3600000) return `${Math.round(diffMs / 60000)}m`;
|
|
52
|
+
return `${Math.round(diffMs / 3600000)}h`;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const truncateCommand = (command, maxLength = 50) => {
|
|
56
|
+
if (command.length <= maxLength) return command;
|
|
57
|
+
return command.substring(0, maxLength - 3) + '...';
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function BackgroundProcessList({
|
|
61
|
+
processes,
|
|
62
|
+
isActive,
|
|
63
|
+
onKill,
|
|
64
|
+
onClose,
|
|
65
|
+
onSelect,
|
|
66
|
+
onToggleFocus
|
|
67
|
+
}) {
|
|
68
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
69
|
+
|
|
70
|
+
// 프로세스 목록이 변경되면 선택 인덱스 조정
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (selectedIndex >= processes.length) {
|
|
73
|
+
setSelectedIndex(Math.max(0, processes.length - 1));
|
|
74
|
+
}
|
|
75
|
+
}, [processes.length, selectedIndex]);
|
|
76
|
+
|
|
77
|
+
// 키보드 입력 처리
|
|
78
|
+
useInput((input, key) => {
|
|
79
|
+
if (!isActive) return;
|
|
80
|
+
|
|
81
|
+
if (key.upArrow) {
|
|
82
|
+
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
83
|
+
} else if (key.downArrow) {
|
|
84
|
+
setSelectedIndex(prev => Math.min(processes.length - 1, prev + 1));
|
|
85
|
+
} else if (input === 'k' || input === 'K') {
|
|
86
|
+
const selected = processes[selectedIndex];
|
|
87
|
+
if (selected && selected.status === 'running' && onKill) {
|
|
88
|
+
onKill(selected.id);
|
|
89
|
+
}
|
|
90
|
+
} else if (input === 'q' || input === 'Q' || key.escape) {
|
|
91
|
+
if (onClose) onClose();
|
|
92
|
+
} else if (key.return) {
|
|
93
|
+
const selected = processes[selectedIndex];
|
|
94
|
+
if (selected && onSelect) {
|
|
95
|
+
onSelect(selected);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}, { isActive });
|
|
99
|
+
|
|
100
|
+
if (!processes || processes.length === 0) {
|
|
101
|
+
return React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
102
|
+
React.createElement(Text, { color: theme.text.secondary },
|
|
103
|
+
'No background processes'
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
109
|
+
// 프로세스 목록
|
|
110
|
+
...processes.map((proc, index) => {
|
|
111
|
+
const isSelected = index === selectedIndex && isActive;
|
|
112
|
+
const statusIcon = getStatusIcon(proc.status);
|
|
113
|
+
const statusColor = getStatusColor(proc.status);
|
|
114
|
+
const duration = formatDuration(proc.startedAt, proc.endedAt);
|
|
115
|
+
const command = truncateCommand(proc.command);
|
|
116
|
+
|
|
117
|
+
return React.createElement(Box, {
|
|
118
|
+
key: proc.id,
|
|
119
|
+
marginLeft: 1
|
|
120
|
+
},
|
|
121
|
+
// 선택 표시
|
|
122
|
+
React.createElement(Text, {
|
|
123
|
+
color: isSelected ? theme.brand.light : theme.text.secondary
|
|
124
|
+
}, isSelected ? '▸ ' : ' '),
|
|
125
|
+
|
|
126
|
+
// 상태 아이콘
|
|
127
|
+
React.createElement(Text, { color: statusColor }, statusIcon),
|
|
128
|
+
React.createElement(Text, null, ' '),
|
|
129
|
+
|
|
130
|
+
// ID
|
|
131
|
+
React.createElement(Text, {
|
|
132
|
+
color: theme.text.secondary,
|
|
133
|
+
dimColor: !isSelected
|
|
134
|
+
}, `[${proc.id}]`),
|
|
135
|
+
React.createElement(Text, null, ' '),
|
|
136
|
+
|
|
137
|
+
// PID
|
|
138
|
+
React.createElement(Text, {
|
|
139
|
+
color: theme.text.secondary,
|
|
140
|
+
dimColor: !isSelected
|
|
141
|
+
}, `pid:${proc.pid}`),
|
|
142
|
+
React.createElement(Text, null, ' '),
|
|
143
|
+
|
|
144
|
+
// 시간
|
|
145
|
+
React.createElement(Text, {
|
|
146
|
+
color: proc.status === 'running' ? theme.brand.light : theme.text.secondary,
|
|
147
|
+
dimColor: !isSelected && proc.status !== 'running'
|
|
148
|
+
}, duration),
|
|
149
|
+
React.createElement(Text, null, ' '),
|
|
150
|
+
|
|
151
|
+
// 명령어
|
|
152
|
+
React.createElement(Text, {
|
|
153
|
+
color: isSelected ? theme.text.primary : theme.text.secondary,
|
|
154
|
+
bold: isSelected
|
|
155
|
+
}, command)
|
|
156
|
+
);
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 간단한 백그라운드 프로세스 상태 표시 (헤더용)
|
|
163
|
+
*/
|
|
164
|
+
export function BackgroundProcessBadge({ count, onClick }) {
|
|
165
|
+
if (count === 0) return null;
|
|
166
|
+
|
|
167
|
+
return React.createElement(Box, {
|
|
168
|
+
marginLeft: 1
|
|
169
|
+
},
|
|
170
|
+
React.createElement(Text, {
|
|
171
|
+
color: theme.brand.light,
|
|
172
|
+
bold: true
|
|
173
|
+
}, `[BG: ${count}]`)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -12,7 +12,7 @@ import { getToolDisplayName, formatToolCall } from '../../system/tool_registry.j
|
|
|
12
12
|
import { getFileSnapshot } from '../../system/file_integrity.js';
|
|
13
13
|
import { safeMkdir, safeAppendFile } from '../../util/safe_fs.js';
|
|
14
14
|
import { resolve, join, dirname } from 'path';
|
|
15
|
-
import { DEBUG_LOG_DIR } from '../../util/config.js';
|
|
15
|
+
import { DEBUG_LOG_DIR, isDebugModeEnabled } from '../../util/config.js';
|
|
16
16
|
import { createHash } from 'crypto';
|
|
17
17
|
import { createDebugLogger } from '../../util/debug_log.js';
|
|
18
18
|
import { prepareEditReplaceDiff } from '../utils/diffUtils.js';
|
|
@@ -59,15 +59,17 @@ function logOldStringEvidence(evidence) {
|
|
|
59
59
|
logEntry += JSON.stringify(evidence.resultArgs, null, 2) + '\n';
|
|
60
60
|
|
|
61
61
|
// Convert sync operations to async (non-blocking)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
if (isDebugModeEnabled()) {
|
|
63
|
+
setImmediate(async () => {
|
|
64
|
+
try {
|
|
65
|
+
// 디렉토리가 없으면 생성 (recursive: true 이므로 이미 존재해도 에러 없음)
|
|
66
|
+
await safeMkdir(dir, { recursive: true });
|
|
67
|
+
await safeAppendFile(OLDSTRING_LOG_FILE, logEntry);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
// Ignore logging errors
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
71
73
|
} catch (err) {
|
|
72
74
|
// Ignore logging errors
|
|
73
75
|
}
|
|
@@ -85,6 +87,7 @@ const TYPE_CONFIG = {
|
|
|
85
87
|
thinking: { icon: '◇ ', color: theme.text.link, bold: false },
|
|
86
88
|
code_execution: { icon: '• ', color: theme.status.warning, bold: false },
|
|
87
89
|
code_result: { icon: '◀ ', color: theme.status.success, bold: false },
|
|
90
|
+
skill_invoked: { icon: '• ', color: theme.text.accent, bold: true },
|
|
88
91
|
default: { icon: '• ', color: theme.text.primary, bold: false }
|
|
89
92
|
};
|
|
90
93
|
|
|
@@ -590,6 +593,19 @@ function StandardDisplay({ item, isPending, hasFollowingResult, nextItem, isLast
|
|
|
590
593
|
);
|
|
591
594
|
}
|
|
592
595
|
|
|
596
|
+
// 스킬 실행 알림 - 녹색 굵은 글씨
|
|
597
|
+
if (type === 'skill_invoked') {
|
|
598
|
+
return React.createElement(Box, { flexDirection: "row", marginBottom, marginTop, marginLeft: 2, width: '100%' },
|
|
599
|
+
React.createElement(Text, {
|
|
600
|
+
color: config.color,
|
|
601
|
+
bold: config.bold
|
|
602
|
+
}, config.icon),
|
|
603
|
+
React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
|
|
604
|
+
React.createElement(Text, { color: theme.text.accent, bold: true }, text)
|
|
605
|
+
)
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
593
609
|
// edit_file_range와 edit_file_replace: 성공한 경우와 denied된 경우 tool_result를 숨김
|
|
594
610
|
if (type === 'tool_result' && (toolName === 'edit_file_range' || toolName === 'edit_file_replace')) {
|
|
595
611
|
const originalResult = item.result?.originalResult;
|
|
@@ -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
|
})
|