snow-ai 0.3.22 → 0.3.24
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.
- package/dist/api/gemini.d.ts +5 -1
- package/dist/api/gemini.js +30 -5
- package/dist/api/responses.js +18 -3
- package/dist/hooks/useConversation.d.ts +0 -5
- package/dist/hooks/useConversation.js +109 -56
- package/dist/hooks/useFilePicker.d.ts +1 -1
- package/dist/hooks/useFilePicker.js +13 -7
- package/dist/hooks/useHistoryNavigation.js +14 -7
- package/dist/hooks/useInputBuffer.d.ts +1 -1
- package/dist/hooks/useInputBuffer.js +22 -6
- package/dist/hooks/useStreamingState.js +2 -2
- package/dist/hooks/useVSCodeState.js +23 -6
- package/dist/mcp/filesystem.js +1 -1
- package/dist/ui/components/ChatInput.js +17 -11
- package/dist/ui/components/MessageList.d.ts +0 -1
- package/dist/ui/components/MessageList.js +1 -2
- package/dist/ui/components/SessionListPanel.js +12 -8
- package/dist/ui/components/SessionListScreen.js +2 -1
- package/dist/ui/components/ToolConfirmation.d.ts +1 -1
- package/dist/ui/components/ToolConfirmation.js +63 -22
- package/dist/ui/components/ToolResultPreview.js +33 -6
- package/dist/ui/pages/ChatScreen.js +21 -17
- package/dist/ui/pages/ConfigScreen.js +167 -16
- package/dist/ui/pages/HeadlessModeScreen.js +0 -1
- package/dist/ui/pages/ProxyConfigScreen.d.ts +1 -1
- package/dist/ui/pages/ProxyConfigScreen.js +6 -6
- package/dist/ui/pages/SensitiveCommandConfigScreen.d.ts +7 -0
- package/dist/ui/pages/SensitiveCommandConfigScreen.js +262 -0
- package/dist/ui/pages/SubAgentConfigScreen.js +1 -1
- package/dist/ui/pages/WelcomeScreen.js +14 -3
- package/dist/utils/apiConfig.d.ts +10 -0
- package/dist/utils/sensitiveCommandManager.d.ts +53 -0
- package/dist/utils/sensitiveCommandManager.js +308 -0
- package/dist/utils/sessionConverter.js +16 -11
- package/package.json +4 -2
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
4
|
+
const CONFIG_DIR = join(homedir(), '.snow');
|
|
5
|
+
const SENSITIVE_COMMANDS_FILE = join(CONFIG_DIR, 'sensitive-commands.json');
|
|
6
|
+
/**
|
|
7
|
+
* 预设的常见敏感指令
|
|
8
|
+
*/
|
|
9
|
+
export const PRESET_SENSITIVE_COMMANDS = [
|
|
10
|
+
{
|
|
11
|
+
id: 'rm',
|
|
12
|
+
pattern: 'rm*',
|
|
13
|
+
description: 'Delete files or directories (rm, rm -rf, etc.)',
|
|
14
|
+
enabled: true,
|
|
15
|
+
isPreset: true,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'rmdir',
|
|
19
|
+
pattern: 'rmdir*',
|
|
20
|
+
description: 'Remove directories',
|
|
21
|
+
enabled: true,
|
|
22
|
+
isPreset: true,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'mv-to-trash',
|
|
26
|
+
pattern: 'mv * /tmp*',
|
|
27
|
+
description: 'Move files to trash/tmp (potential data loss)',
|
|
28
|
+
enabled: true,
|
|
29
|
+
isPreset: true,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'chmod',
|
|
33
|
+
pattern: 'chmod*',
|
|
34
|
+
description: 'Change file permissions',
|
|
35
|
+
enabled: true,
|
|
36
|
+
isPreset: true,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'chown',
|
|
40
|
+
pattern: 'chown*',
|
|
41
|
+
description: 'Change file ownership',
|
|
42
|
+
enabled: true,
|
|
43
|
+
isPreset: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'dd',
|
|
47
|
+
pattern: 'dd*',
|
|
48
|
+
description: 'Low-level data copy (disk operations)',
|
|
49
|
+
enabled: true,
|
|
50
|
+
isPreset: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'mkfs',
|
|
54
|
+
pattern: 'mkfs*',
|
|
55
|
+
description: 'Format filesystem',
|
|
56
|
+
enabled: true,
|
|
57
|
+
isPreset: true,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'fdisk',
|
|
61
|
+
pattern: 'fdisk*',
|
|
62
|
+
description: 'Disk partition manipulation',
|
|
63
|
+
enabled: true,
|
|
64
|
+
isPreset: true,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'killall',
|
|
68
|
+
pattern: 'killall*',
|
|
69
|
+
description: 'Kill all processes by name',
|
|
70
|
+
enabled: true,
|
|
71
|
+
isPreset: true,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'pkill',
|
|
75
|
+
pattern: 'pkill*',
|
|
76
|
+
description: 'Kill processes by pattern',
|
|
77
|
+
enabled: true,
|
|
78
|
+
isPreset: true,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'reboot',
|
|
82
|
+
pattern: 'reboot*',
|
|
83
|
+
description: 'Reboot the system',
|
|
84
|
+
enabled: true,
|
|
85
|
+
isPreset: true,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'shutdown',
|
|
89
|
+
pattern: 'shutdown*',
|
|
90
|
+
description: 'Shutdown the system',
|
|
91
|
+
enabled: true,
|
|
92
|
+
isPreset: true,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'sudo',
|
|
96
|
+
pattern: 'sudo*',
|
|
97
|
+
description: 'Execute commands with superuser privileges',
|
|
98
|
+
enabled: true,
|
|
99
|
+
isPreset: true,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'su',
|
|
103
|
+
pattern: 'su*',
|
|
104
|
+
description: 'Switch user',
|
|
105
|
+
enabled: true,
|
|
106
|
+
isPreset: true,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'curl-post',
|
|
110
|
+
pattern: 'curl*-X POST*',
|
|
111
|
+
description: 'HTTP POST requests (potential data transmission)',
|
|
112
|
+
enabled: false,
|
|
113
|
+
isPreset: true,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'wget',
|
|
117
|
+
pattern: 'wget*',
|
|
118
|
+
description: 'Download files from internet',
|
|
119
|
+
enabled: false,
|
|
120
|
+
isPreset: true,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'git-push',
|
|
124
|
+
pattern: 'git push*',
|
|
125
|
+
description: 'Push code to remote repository',
|
|
126
|
+
enabled: false,
|
|
127
|
+
isPreset: true,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'git-force-push',
|
|
131
|
+
pattern: 'git push*--force*',
|
|
132
|
+
description: 'Force push to remote repository (destructive)',
|
|
133
|
+
enabled: true,
|
|
134
|
+
isPreset: true,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'npm-publish',
|
|
138
|
+
pattern: 'npm publish*',
|
|
139
|
+
description: 'Publish package to npm registry',
|
|
140
|
+
enabled: true,
|
|
141
|
+
isPreset: true,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'docker-rm',
|
|
145
|
+
pattern: 'docker rm*',
|
|
146
|
+
description: 'Remove Docker containers',
|
|
147
|
+
enabled: false,
|
|
148
|
+
isPreset: true,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'docker-rmi',
|
|
152
|
+
pattern: 'docker rmi*',
|
|
153
|
+
description: 'Remove Docker images',
|
|
154
|
+
enabled: false,
|
|
155
|
+
isPreset: true,
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
/**
|
|
159
|
+
* Ensure config directory exists
|
|
160
|
+
*/
|
|
161
|
+
function ensureConfigDirectory() {
|
|
162
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
163
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Load sensitive commands configuration
|
|
168
|
+
*/
|
|
169
|
+
export function loadSensitiveCommands() {
|
|
170
|
+
ensureConfigDirectory();
|
|
171
|
+
if (!existsSync(SENSITIVE_COMMANDS_FILE)) {
|
|
172
|
+
// 首次加载,使用预设配置
|
|
173
|
+
const defaultConfig = {
|
|
174
|
+
commands: [...PRESET_SENSITIVE_COMMANDS],
|
|
175
|
+
};
|
|
176
|
+
saveSensitiveCommands(defaultConfig);
|
|
177
|
+
return defaultConfig;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const configData = readFileSync(SENSITIVE_COMMANDS_FILE, 'utf8');
|
|
181
|
+
const config = JSON.parse(configData);
|
|
182
|
+
// 合并预设命令(处理新增的预设命令)
|
|
183
|
+
const existingIds = new Set(config.commands.map(cmd => cmd.id));
|
|
184
|
+
const newPresets = PRESET_SENSITIVE_COMMANDS.filter(preset => !existingIds.has(preset.id));
|
|
185
|
+
if (newPresets.length > 0) {
|
|
186
|
+
config.commands = [...config.commands, ...newPresets];
|
|
187
|
+
saveSensitiveCommands(config);
|
|
188
|
+
}
|
|
189
|
+
return config;
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
console.error('Failed to load sensitive commands config:', error);
|
|
193
|
+
return { commands: [...PRESET_SENSITIVE_COMMANDS] };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Save sensitive commands configuration
|
|
198
|
+
*/
|
|
199
|
+
export function saveSensitiveCommands(config) {
|
|
200
|
+
ensureConfigDirectory();
|
|
201
|
+
try {
|
|
202
|
+
const configData = JSON.stringify(config, null, 2);
|
|
203
|
+
writeFileSync(SENSITIVE_COMMANDS_FILE, configData, 'utf8');
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
throw new Error(`Failed to save sensitive commands config: ${error}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Add a custom sensitive command
|
|
211
|
+
*/
|
|
212
|
+
export function addSensitiveCommand(pattern, description) {
|
|
213
|
+
const config = loadSensitiveCommands();
|
|
214
|
+
// 生成唯一ID
|
|
215
|
+
const id = `custom-${Date.now()}-${Math.random()
|
|
216
|
+
.toString(36)
|
|
217
|
+
.substring(2, 9)}`;
|
|
218
|
+
config.commands.push({
|
|
219
|
+
id,
|
|
220
|
+
pattern,
|
|
221
|
+
description,
|
|
222
|
+
enabled: true,
|
|
223
|
+
isPreset: false,
|
|
224
|
+
});
|
|
225
|
+
saveSensitiveCommands(config);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Remove a sensitive command
|
|
229
|
+
*/
|
|
230
|
+
export function removeSensitiveCommand(id) {
|
|
231
|
+
const config = loadSensitiveCommands();
|
|
232
|
+
config.commands = config.commands.filter(cmd => cmd.id !== id);
|
|
233
|
+
saveSensitiveCommands(config);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Update a sensitive command
|
|
237
|
+
*/
|
|
238
|
+
export function updateSensitiveCommand(id, updates) {
|
|
239
|
+
const config = loadSensitiveCommands();
|
|
240
|
+
const commandIndex = config.commands.findIndex(cmd => cmd.id === id);
|
|
241
|
+
if (commandIndex === -1) {
|
|
242
|
+
throw new Error(`Sensitive command with id "${id}" not found`);
|
|
243
|
+
}
|
|
244
|
+
const existingCommand = config.commands[commandIndex];
|
|
245
|
+
config.commands[commandIndex] = {
|
|
246
|
+
...existingCommand,
|
|
247
|
+
...updates,
|
|
248
|
+
id: existingCommand.id,
|
|
249
|
+
isPreset: existingCommand.isPreset,
|
|
250
|
+
};
|
|
251
|
+
saveSensitiveCommands(config);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Toggle a sensitive command enabled state
|
|
255
|
+
*/
|
|
256
|
+
export function toggleSensitiveCommand(id) {
|
|
257
|
+
const config = loadSensitiveCommands();
|
|
258
|
+
const command = config.commands.find(cmd => cmd.id === id);
|
|
259
|
+
if (!command) {
|
|
260
|
+
throw new Error(`Sensitive command with id "${id}" not found`);
|
|
261
|
+
}
|
|
262
|
+
command.enabled = !command.enabled;
|
|
263
|
+
saveSensitiveCommands(config);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* 将通配符模式转换为正则表达式
|
|
267
|
+
* 支持 * 通配符
|
|
268
|
+
*/
|
|
269
|
+
function patternToRegex(pattern) {
|
|
270
|
+
// 转义特殊字符,除了 * 和空格
|
|
271
|
+
const escaped = pattern
|
|
272
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
273
|
+
.replace(/\*/g, '.*');
|
|
274
|
+
// 使用 ^ 和 $ 确保完整匹配,但允许参数
|
|
275
|
+
return new RegExp(`^${escaped}`, 'i');
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Check if a command matches any enabled sensitive pattern
|
|
279
|
+
*/
|
|
280
|
+
export function isSensitiveCommand(command) {
|
|
281
|
+
const config = loadSensitiveCommands();
|
|
282
|
+
const enabledCommands = config.commands.filter(cmd => cmd.enabled);
|
|
283
|
+
// 清理命令,移除多余的空格
|
|
284
|
+
const cleanCommand = command.trim().replace(/\s+/g, ' ');
|
|
285
|
+
for (const cmd of enabledCommands) {
|
|
286
|
+
const regex = patternToRegex(cmd.pattern);
|
|
287
|
+
if (regex.test(cleanCommand)) {
|
|
288
|
+
return { isSensitive: true, matchedCommand: cmd };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { isSensitive: false };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get all sensitive commands
|
|
295
|
+
*/
|
|
296
|
+
export function getAllSensitiveCommands() {
|
|
297
|
+
const config = loadSensitiveCommands();
|
|
298
|
+
return config.commands;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Reset to default preset commands
|
|
302
|
+
*/
|
|
303
|
+
export function resetToDefaults() {
|
|
304
|
+
const config = {
|
|
305
|
+
commands: [...PRESET_SENSITIVE_COMMANDS],
|
|
306
|
+
};
|
|
307
|
+
saveSensitiveCommands(config);
|
|
308
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatToolCallMessage } from './messageFormatter.js';
|
|
2
|
+
import { isToolNeedTwoStepDisplay } from './toolDisplayConfig.js';
|
|
2
3
|
/**
|
|
3
4
|
* Convert API format session messages to UI format messages
|
|
4
5
|
* Process messages in order to maintain correct sequence
|
|
@@ -114,17 +115,21 @@ export function convertSessionMessagesToUI(sessionMessages) {
|
|
|
114
115
|
catch (e) {
|
|
115
116
|
toolArgs = {};
|
|
116
117
|
}
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
118
|
+
// Only add "in progress" message for tools that need two-step display
|
|
119
|
+
const needTwoSteps = isToolNeedTwoStepDisplay(toolCall.function.name);
|
|
120
|
+
if (needTwoSteps) {
|
|
121
|
+
// Add tool call message (in progress)
|
|
122
|
+
uiMessages.push({
|
|
123
|
+
role: 'assistant',
|
|
124
|
+
content: `⚡ ${toolDisplay.toolName}`,
|
|
125
|
+
streaming: false,
|
|
126
|
+
toolCall: {
|
|
127
|
+
name: toolCall.function.name,
|
|
128
|
+
arguments: toolArgs,
|
|
129
|
+
},
|
|
130
|
+
toolDisplay,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
128
133
|
processedToolCalls.add(toolCall.id);
|
|
129
134
|
}
|
|
130
135
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snow-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
4
4
|
"description": "Intelligent Command Line Assistant powered by AI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -89,7 +89,9 @@
|
|
|
89
89
|
"extends": "xo-react",
|
|
90
90
|
"prettier": true,
|
|
91
91
|
"rules": {
|
|
92
|
-
"react/prop-types": "off"
|
|
92
|
+
"react/prop-types": "off",
|
|
93
|
+
"react-hooks/rules-of-hooks": "error",
|
|
94
|
+
"react-hooks/exhaustive-deps": "warn"
|
|
93
95
|
}
|
|
94
96
|
},
|
|
95
97
|
"prettier": "@vdemedes/prettier-config"
|