snow-ai 0.1.12 → 0.2.1
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/chat.d.ts +65 -2
- package/dist/api/chat.js +299 -16
- package/dist/api/responses.d.ts +52 -0
- package/dist/api/responses.js +541 -0
- package/dist/api/systemPrompt.d.ts +4 -0
- package/dist/api/systemPrompt.js +43 -0
- package/dist/app.js +15 -4
- package/dist/cli.js +17 -1
- package/dist/hooks/useConversation.d.ts +32 -0
- package/dist/hooks/useConversation.js +403 -0
- package/dist/hooks/useGlobalNavigation.d.ts +6 -0
- package/dist/hooks/useGlobalNavigation.js +15 -0
- package/dist/hooks/useSessionManagement.d.ts +10 -0
- package/dist/hooks/useSessionManagement.js +43 -0
- package/dist/hooks/useSessionSave.d.ts +8 -0
- package/dist/hooks/useSessionSave.js +52 -0
- package/dist/hooks/useToolConfirmation.d.ts +18 -0
- package/dist/hooks/useToolConfirmation.js +49 -0
- package/dist/mcp/bash.d.ts +57 -0
- package/dist/mcp/bash.js +138 -0
- package/dist/mcp/filesystem.d.ts +307 -0
- package/dist/mcp/filesystem.js +520 -0
- package/dist/mcp/todo.d.ts +55 -0
- package/dist/mcp/todo.js +329 -0
- package/dist/test/logger-test.d.ts +1 -0
- package/dist/test/logger-test.js +7 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/ui/components/ChatInput.d.ts +15 -2
- package/dist/ui/components/ChatInput.js +445 -59
- package/dist/ui/components/CommandPanel.d.ts +2 -2
- package/dist/ui/components/CommandPanel.js +11 -7
- package/dist/ui/components/DiffViewer.d.ts +9 -0
- package/dist/ui/components/DiffViewer.js +93 -0
- package/dist/ui/components/FileList.d.ts +14 -0
- package/dist/ui/components/FileList.js +131 -0
- package/dist/ui/components/MCPInfoPanel.d.ts +2 -0
- package/dist/ui/components/MCPInfoPanel.js +74 -0
- package/dist/ui/components/MCPInfoScreen.d.ts +7 -0
- package/dist/ui/components/MCPInfoScreen.js +27 -0
- package/dist/ui/components/MarkdownRenderer.d.ts +7 -0
- package/dist/ui/components/MarkdownRenderer.js +110 -0
- package/dist/ui/components/Menu.d.ts +5 -2
- package/dist/ui/components/Menu.js +60 -9
- package/dist/ui/components/MessageList.d.ts +30 -2
- package/dist/ui/components/MessageList.js +64 -12
- package/dist/ui/components/PendingMessages.js +1 -1
- package/dist/ui/components/ScrollableSelectInput.d.ts +29 -0
- package/dist/ui/components/ScrollableSelectInput.js +157 -0
- package/dist/ui/components/SessionListScreen.d.ts +7 -0
- package/dist/ui/components/SessionListScreen.js +196 -0
- package/dist/ui/components/SessionListScreenWrapper.d.ts +7 -0
- package/dist/ui/components/SessionListScreenWrapper.js +14 -0
- package/dist/ui/components/TodoTree.d.ts +15 -0
- package/dist/ui/components/TodoTree.js +60 -0
- package/dist/ui/components/ToolConfirmation.d.ts +8 -0
- package/dist/ui/components/ToolConfirmation.js +38 -0
- package/dist/ui/components/ToolResultPreview.d.ts +12 -0
- package/dist/ui/components/ToolResultPreview.js +115 -0
- package/dist/ui/pages/ChatScreen.d.ts +4 -0
- package/dist/ui/pages/ChatScreen.js +385 -196
- package/dist/ui/pages/MCPConfigScreen.d.ts +6 -0
- package/dist/ui/pages/MCPConfigScreen.js +55 -0
- package/dist/ui/pages/ModelConfigScreen.js +73 -12
- package/dist/ui/pages/WelcomeScreen.js +17 -11
- package/dist/utils/apiConfig.d.ts +12 -0
- package/dist/utils/apiConfig.js +95 -9
- package/dist/utils/commandExecutor.d.ts +2 -1
- package/dist/utils/commands/init.d.ts +2 -0
- package/dist/utils/commands/init.js +93 -0
- package/dist/utils/commands/mcp.d.ts +2 -0
- package/dist/utils/commands/mcp.js +12 -0
- package/dist/utils/commands/resume.d.ts +2 -0
- package/dist/utils/commands/resume.js +12 -0
- package/dist/utils/commands/yolo.d.ts +2 -0
- package/dist/utils/commands/yolo.js +12 -0
- package/dist/utils/fileUtils.d.ts +44 -0
- package/dist/utils/fileUtils.js +222 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/logger.d.ts +31 -0
- package/dist/utils/logger.js +97 -0
- package/dist/utils/mcpToolsManager.d.ts +47 -0
- package/dist/utils/mcpToolsManager.js +476 -0
- package/dist/utils/messageFormatter.d.ts +12 -0
- package/dist/utils/messageFormatter.js +32 -0
- package/dist/utils/sessionConverter.d.ts +6 -0
- package/dist/utils/sessionConverter.js +61 -0
- package/dist/utils/sessionManager.d.ts +39 -0
- package/dist/utils/sessionManager.js +141 -0
- package/dist/utils/textBuffer.d.ts +36 -7
- package/dist/utils/textBuffer.js +265 -179
- package/dist/utils/todoPreprocessor.d.ts +5 -0
- package/dist/utils/todoPreprocessor.js +19 -0
- package/dist/utils/toolExecutor.d.ts +21 -0
- package/dist/utils/toolExecutor.js +28 -0
- package/package.json +12 -3
- package/readme.md +2 -2
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
4
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
5
|
+
import { getMCPConfig } from './apiConfig.js';
|
|
6
|
+
import { mcpTools as filesystemTools } from '../mcp/filesystem.js';
|
|
7
|
+
import { mcpTools as terminalTools } from '../mcp/bash.js';
|
|
8
|
+
import { TodoService } from '../mcp/todo.js';
|
|
9
|
+
import { sessionManager } from './sessionManager.js';
|
|
10
|
+
import { logger } from './logger.js';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
let toolsCache = null;
|
|
14
|
+
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
15
|
+
// Initialize TODO service with sessionManager accessor
|
|
16
|
+
const todoService = new TodoService(path.join(os.homedir(), '.snow'), () => {
|
|
17
|
+
const session = sessionManager.getCurrentSession();
|
|
18
|
+
return session ? session.id : null;
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Get the TODO service instance
|
|
22
|
+
*/
|
|
23
|
+
export function getTodoService() {
|
|
24
|
+
return todoService;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Generate a hash of the current MCP configuration
|
|
28
|
+
*/
|
|
29
|
+
function generateConfigHash() {
|
|
30
|
+
try {
|
|
31
|
+
const mcpConfig = getMCPConfig();
|
|
32
|
+
return JSON.stringify(mcpConfig.mcpServers);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if the cache is valid and not expired
|
|
40
|
+
*/
|
|
41
|
+
function isCacheValid() {
|
|
42
|
+
if (!toolsCache)
|
|
43
|
+
return false;
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const isExpired = (now - toolsCache.lastUpdate) > CACHE_DURATION;
|
|
46
|
+
const configChanged = toolsCache.configHash !== generateConfigHash();
|
|
47
|
+
return !isExpired && !configChanged;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get cached tools or build cache if needed
|
|
51
|
+
*/
|
|
52
|
+
async function getCachedTools() {
|
|
53
|
+
if (isCacheValid()) {
|
|
54
|
+
return toolsCache.tools;
|
|
55
|
+
}
|
|
56
|
+
await refreshToolsCache();
|
|
57
|
+
return toolsCache.tools;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Refresh the tools cache by collecting all available tools
|
|
61
|
+
*/
|
|
62
|
+
async function refreshToolsCache() {
|
|
63
|
+
const allTools = [];
|
|
64
|
+
const servicesInfo = [];
|
|
65
|
+
// Add built-in filesystem tools (always available)
|
|
66
|
+
const filesystemServiceTools = filesystemTools.map(tool => ({
|
|
67
|
+
name: tool.name.replace('filesystem_', ''),
|
|
68
|
+
description: tool.description,
|
|
69
|
+
inputSchema: tool.inputSchema
|
|
70
|
+
}));
|
|
71
|
+
servicesInfo.push({
|
|
72
|
+
serviceName: 'filesystem',
|
|
73
|
+
tools: filesystemServiceTools,
|
|
74
|
+
isBuiltIn: true,
|
|
75
|
+
connected: true
|
|
76
|
+
});
|
|
77
|
+
for (const tool of filesystemTools) {
|
|
78
|
+
allTools.push({
|
|
79
|
+
type: "function",
|
|
80
|
+
function: {
|
|
81
|
+
name: `filesystem-${tool.name.replace('filesystem_', '')}`,
|
|
82
|
+
description: tool.description,
|
|
83
|
+
parameters: tool.inputSchema
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Add built-in terminal tools (always available)
|
|
88
|
+
const terminalServiceTools = terminalTools.map(tool => ({
|
|
89
|
+
name: tool.name.replace('terminal_', ''),
|
|
90
|
+
description: tool.description,
|
|
91
|
+
inputSchema: tool.inputSchema
|
|
92
|
+
}));
|
|
93
|
+
servicesInfo.push({
|
|
94
|
+
serviceName: 'terminal',
|
|
95
|
+
tools: terminalServiceTools,
|
|
96
|
+
isBuiltIn: true,
|
|
97
|
+
connected: true
|
|
98
|
+
});
|
|
99
|
+
for (const tool of terminalTools) {
|
|
100
|
+
allTools.push({
|
|
101
|
+
type: "function",
|
|
102
|
+
function: {
|
|
103
|
+
name: `terminal-${tool.name.replace('terminal_', '')}`,
|
|
104
|
+
description: tool.description,
|
|
105
|
+
parameters: tool.inputSchema
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// Add built-in TODO tools (always available)
|
|
110
|
+
await todoService.initialize();
|
|
111
|
+
const todoTools = todoService.getTools();
|
|
112
|
+
const todoServiceTools = todoTools.map(tool => ({
|
|
113
|
+
name: tool.name.replace('todo-', ''),
|
|
114
|
+
description: tool.description || '',
|
|
115
|
+
inputSchema: tool.inputSchema
|
|
116
|
+
}));
|
|
117
|
+
servicesInfo.push({
|
|
118
|
+
serviceName: 'todo',
|
|
119
|
+
tools: todoServiceTools,
|
|
120
|
+
isBuiltIn: true,
|
|
121
|
+
connected: true
|
|
122
|
+
});
|
|
123
|
+
for (const tool of todoTools) {
|
|
124
|
+
allTools.push({
|
|
125
|
+
type: "function",
|
|
126
|
+
function: {
|
|
127
|
+
name: tool.name,
|
|
128
|
+
description: tool.description || '',
|
|
129
|
+
parameters: tool.inputSchema
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// Add user-configured MCP server tools (probe for availability but don't maintain connections)
|
|
134
|
+
try {
|
|
135
|
+
const mcpConfig = getMCPConfig();
|
|
136
|
+
for (const [serviceName, server] of Object.entries(mcpConfig.mcpServers)) {
|
|
137
|
+
try {
|
|
138
|
+
const serviceTools = await probeServiceTools(serviceName, server);
|
|
139
|
+
servicesInfo.push({
|
|
140
|
+
serviceName,
|
|
141
|
+
tools: serviceTools,
|
|
142
|
+
isBuiltIn: false,
|
|
143
|
+
connected: true
|
|
144
|
+
});
|
|
145
|
+
for (const tool of serviceTools) {
|
|
146
|
+
allTools.push({
|
|
147
|
+
type: "function",
|
|
148
|
+
function: {
|
|
149
|
+
name: `${serviceName}-${tool.name}`,
|
|
150
|
+
description: tool.description,
|
|
151
|
+
parameters: tool.inputSchema
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
servicesInfo.push({
|
|
158
|
+
serviceName,
|
|
159
|
+
tools: [],
|
|
160
|
+
isBuiltIn: false,
|
|
161
|
+
connected: false,
|
|
162
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
logger.warn('Failed to load MCP config:', error);
|
|
169
|
+
}
|
|
170
|
+
// Update cache
|
|
171
|
+
toolsCache = {
|
|
172
|
+
tools: allTools,
|
|
173
|
+
servicesInfo,
|
|
174
|
+
lastUpdate: Date.now(),
|
|
175
|
+
configHash: generateConfigHash()
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Manually refresh the tools cache (for configuration changes)
|
|
180
|
+
*/
|
|
181
|
+
export async function refreshMCPToolsCache() {
|
|
182
|
+
toolsCache = null;
|
|
183
|
+
await refreshToolsCache();
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Clear the tools cache (useful for testing or forcing refresh)
|
|
187
|
+
*/
|
|
188
|
+
export function clearMCPToolsCache() {
|
|
189
|
+
toolsCache = null;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Collect all available MCP tools from built-in and user-configured services
|
|
193
|
+
* Uses caching to avoid reconnecting on every message
|
|
194
|
+
*/
|
|
195
|
+
export async function collectAllMCPTools() {
|
|
196
|
+
return await getCachedTools();
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get detailed information about all MCP services and their tools
|
|
200
|
+
* Uses cached data when available
|
|
201
|
+
*/
|
|
202
|
+
export async function getMCPServicesInfo() {
|
|
203
|
+
if (!isCacheValid()) {
|
|
204
|
+
await refreshToolsCache();
|
|
205
|
+
}
|
|
206
|
+
return toolsCache.servicesInfo;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Quick probe of MCP service tools without maintaining connections
|
|
210
|
+
* This is used for caching tool definitions
|
|
211
|
+
*/
|
|
212
|
+
async function probeServiceTools(serviceName, server) {
|
|
213
|
+
return await connectAndGetTools(serviceName, server, 3000); // Short timeout for probing
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Connect to MCP service and get tools (used for both caching and execution)
|
|
217
|
+
* @param serviceName - Name of the service
|
|
218
|
+
* @param server - Server configuration
|
|
219
|
+
* @param timeoutMs - Timeout in milliseconds (default 10000)
|
|
220
|
+
*/
|
|
221
|
+
async function connectAndGetTools(serviceName, server, timeoutMs = 10000) {
|
|
222
|
+
let client = null;
|
|
223
|
+
let transport;
|
|
224
|
+
try {
|
|
225
|
+
client = new Client({
|
|
226
|
+
name: `snow-cli-${serviceName}`,
|
|
227
|
+
version: '1.0.0',
|
|
228
|
+
}, {
|
|
229
|
+
capabilities: {}
|
|
230
|
+
});
|
|
231
|
+
// Create transport based on server configuration
|
|
232
|
+
if (server.url) {
|
|
233
|
+
let urlString = server.url;
|
|
234
|
+
if (server.env) {
|
|
235
|
+
const allEnv = { ...process.env, ...server.env };
|
|
236
|
+
urlString = urlString.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, simple) => {
|
|
237
|
+
const varName = braced || simple;
|
|
238
|
+
return allEnv[varName] || match;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
urlString = urlString.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, simple) => {
|
|
243
|
+
const varName = braced || simple;
|
|
244
|
+
return process.env[varName] || match;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const url = new URL(urlString);
|
|
248
|
+
try {
|
|
249
|
+
// Try HTTP first
|
|
250
|
+
const headers = {
|
|
251
|
+
'Content-Type': 'application/json',
|
|
252
|
+
};
|
|
253
|
+
if (server.env) {
|
|
254
|
+
const allEnv = { ...process.env, ...server.env };
|
|
255
|
+
if (allEnv['MCP_API_KEY']) {
|
|
256
|
+
headers['Authorization'] = `Bearer ${allEnv['MCP_API_KEY']}`;
|
|
257
|
+
}
|
|
258
|
+
if (allEnv['MCP_AUTH_HEADER']) {
|
|
259
|
+
headers['Authorization'] = allEnv['MCP_AUTH_HEADER'];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
transport = new StreamableHTTPClientTransport(url, {
|
|
263
|
+
requestInit: { headers }
|
|
264
|
+
});
|
|
265
|
+
await Promise.race([
|
|
266
|
+
client.connect(transport),
|
|
267
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('HTTP connection timeout')), timeoutMs))
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
catch (httpError) {
|
|
271
|
+
// Fallback to SSE
|
|
272
|
+
try {
|
|
273
|
+
await client.close();
|
|
274
|
+
}
|
|
275
|
+
catch { }
|
|
276
|
+
client = new Client({
|
|
277
|
+
name: `snow-cli-${serviceName}`,
|
|
278
|
+
version: '1.0.0',
|
|
279
|
+
}, {
|
|
280
|
+
capabilities: {}
|
|
281
|
+
});
|
|
282
|
+
transport = new SSEClientTransport(url);
|
|
283
|
+
await Promise.race([
|
|
284
|
+
client.connect(transport),
|
|
285
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('SSE connection timeout')), timeoutMs))
|
|
286
|
+
]);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else if (server.command) {
|
|
290
|
+
const processEnv = {};
|
|
291
|
+
Object.entries(process.env).forEach(([key, value]) => {
|
|
292
|
+
if (value !== undefined) {
|
|
293
|
+
processEnv[key] = value;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
if (server.env) {
|
|
297
|
+
Object.assign(processEnv, server.env);
|
|
298
|
+
}
|
|
299
|
+
transport = new StdioClientTransport({
|
|
300
|
+
command: server.command,
|
|
301
|
+
args: server.args || [],
|
|
302
|
+
env: processEnv,
|
|
303
|
+
});
|
|
304
|
+
await client.connect(transport);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
throw new Error('No URL or command specified');
|
|
308
|
+
}
|
|
309
|
+
// Get tools from the service
|
|
310
|
+
const toolsResult = await Promise.race([
|
|
311
|
+
client.listTools(),
|
|
312
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('ListTools timeout')), timeoutMs))
|
|
313
|
+
]);
|
|
314
|
+
return toolsResult.tools?.map(tool => ({
|
|
315
|
+
name: tool.name,
|
|
316
|
+
description: tool.description || '',
|
|
317
|
+
inputSchema: tool.inputSchema
|
|
318
|
+
})) || [];
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
try {
|
|
322
|
+
if (client) {
|
|
323
|
+
await client.close();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
logger.warn(`Failed to close client for ${serviceName}:`, error);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Execute an MCP tool by parsing the prefixed tool name
|
|
333
|
+
* Only connects to the service when actually needed
|
|
334
|
+
*/
|
|
335
|
+
export async function executeMCPTool(toolName, args) {
|
|
336
|
+
// Find the service name by checking against known services
|
|
337
|
+
let serviceName = null;
|
|
338
|
+
let actualToolName = null;
|
|
339
|
+
// Check built-in services first
|
|
340
|
+
if (toolName.startsWith('todo-')) {
|
|
341
|
+
serviceName = 'todo';
|
|
342
|
+
actualToolName = toolName.substring('todo-'.length);
|
|
343
|
+
}
|
|
344
|
+
else if (toolName.startsWith('filesystem-')) {
|
|
345
|
+
serviceName = 'filesystem';
|
|
346
|
+
actualToolName = toolName.substring('filesystem-'.length);
|
|
347
|
+
}
|
|
348
|
+
else if (toolName.startsWith('terminal-')) {
|
|
349
|
+
serviceName = 'terminal';
|
|
350
|
+
actualToolName = toolName.substring('terminal-'.length);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// Check configured MCP services
|
|
354
|
+
try {
|
|
355
|
+
const mcpConfig = getMCPConfig();
|
|
356
|
+
for (const configuredServiceName of Object.keys(mcpConfig.mcpServers)) {
|
|
357
|
+
const prefix = `${configuredServiceName}-`;
|
|
358
|
+
if (toolName.startsWith(prefix)) {
|
|
359
|
+
serviceName = configuredServiceName;
|
|
360
|
+
actualToolName = toolName.substring(prefix.length);
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// Ignore config errors, will handle below
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!serviceName || !actualToolName) {
|
|
370
|
+
throw new Error(`Invalid tool name format: ${toolName}. Expected format: serviceName-toolName`);
|
|
371
|
+
}
|
|
372
|
+
if (serviceName === 'todo') {
|
|
373
|
+
// Handle built-in TODO tools (no connection needed)
|
|
374
|
+
return await todoService.executeTool(toolName, args);
|
|
375
|
+
}
|
|
376
|
+
else if (serviceName === 'filesystem') {
|
|
377
|
+
// Handle built-in filesystem tools (no connection needed)
|
|
378
|
+
const { filesystemService } = await import('../mcp/filesystem.js');
|
|
379
|
+
switch (actualToolName) {
|
|
380
|
+
case 'read':
|
|
381
|
+
return await filesystemService.getFileContent(args.filePath, args.startLine, args.endLine);
|
|
382
|
+
case 'create':
|
|
383
|
+
return await filesystemService.createFile(args.filePath, args.content, args.createDirectories);
|
|
384
|
+
case 'delete':
|
|
385
|
+
return await filesystemService.deleteFile(args.filePath);
|
|
386
|
+
case 'list':
|
|
387
|
+
return await filesystemService.listFiles(args.dirPath);
|
|
388
|
+
case 'exists':
|
|
389
|
+
return await filesystemService.exists(args.filePath);
|
|
390
|
+
case 'info':
|
|
391
|
+
return await filesystemService.getFileInfo(args.filePath);
|
|
392
|
+
case 'edit':
|
|
393
|
+
return await filesystemService.editFile(args.filePath, args.startLine, args.endLine, args.newContent, args.contextLines);
|
|
394
|
+
case 'search':
|
|
395
|
+
return await filesystemService.searchCode(args.query, args.dirPath, args.fileExtensions, args.caseSensitive, args.maxResults);
|
|
396
|
+
default:
|
|
397
|
+
throw new Error(`Unknown filesystem tool: ${actualToolName}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else if (serviceName === 'terminal') {
|
|
401
|
+
// Handle built-in terminal tools (no connection needed)
|
|
402
|
+
const { terminalService } = await import('../mcp/bash.js');
|
|
403
|
+
switch (actualToolName) {
|
|
404
|
+
case 'execute':
|
|
405
|
+
return await terminalService.executeCommand(args.command, args.timeout);
|
|
406
|
+
default:
|
|
407
|
+
throw new Error(`Unknown terminal tool: ${actualToolName}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
// Handle user-configured MCP service tools - connect only when needed
|
|
412
|
+
const mcpConfig = getMCPConfig();
|
|
413
|
+
const server = mcpConfig.mcpServers[serviceName];
|
|
414
|
+
if (!server) {
|
|
415
|
+
throw new Error(`MCP service not found: ${serviceName}`);
|
|
416
|
+
}
|
|
417
|
+
// Connect to service and execute tool
|
|
418
|
+
logger.info(`Executing tool ${actualToolName} on MCP service ${serviceName}... args: ${args ? JSON.stringify(args) : 'none'}`);
|
|
419
|
+
return await executeOnExternalMCPService(serviceName, server, actualToolName, args);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Execute a tool on an external MCP service - connects only when needed
|
|
424
|
+
*/
|
|
425
|
+
async function executeOnExternalMCPService(serviceName, server, toolName, args) {
|
|
426
|
+
let client = null;
|
|
427
|
+
logger.debug(`Connecting to MCP service ${serviceName} to execute tool ${toolName}...`);
|
|
428
|
+
try {
|
|
429
|
+
client = new Client({
|
|
430
|
+
name: `snow-cli-${serviceName}`,
|
|
431
|
+
version: '1.0.0',
|
|
432
|
+
}, {
|
|
433
|
+
capabilities: {}
|
|
434
|
+
});
|
|
435
|
+
// Setup transport (similar to getServiceTools)
|
|
436
|
+
let transport;
|
|
437
|
+
if (server.url) {
|
|
438
|
+
let urlString = server.url;
|
|
439
|
+
if (server.env) {
|
|
440
|
+
const allEnv = { ...process.env, ...server.env };
|
|
441
|
+
urlString = urlString.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, simple) => {
|
|
442
|
+
const varName = braced || simple;
|
|
443
|
+
return allEnv[varName] || match;
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
const url = new URL(urlString);
|
|
447
|
+
transport = new StreamableHTTPClientTransport(url);
|
|
448
|
+
}
|
|
449
|
+
else if (server.command) {
|
|
450
|
+
transport = new StdioClientTransport({
|
|
451
|
+
command: server.command,
|
|
452
|
+
args: server.args || [],
|
|
453
|
+
env: server.env ? { ...process.env, ...server.env } : process.env,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
await client.connect(transport);
|
|
457
|
+
logger.debug(`ToolName ${toolName}, args:`, args ? JSON.stringify(args) : 'none');
|
|
458
|
+
// Execute the tool with the original tool name (not prefixed)
|
|
459
|
+
const result = await client.callTool({
|
|
460
|
+
name: toolName,
|
|
461
|
+
arguments: args
|
|
462
|
+
});
|
|
463
|
+
logger.debug(`result from ${serviceName} tool ${toolName}:`, result);
|
|
464
|
+
return result.content;
|
|
465
|
+
}
|
|
466
|
+
finally {
|
|
467
|
+
try {
|
|
468
|
+
if (client) {
|
|
469
|
+
await client.close();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
logger.warn(`Failed to close client for ${serviceName}:`, error);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ToolCall } from './toolExecutor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Format tool call display information for UI rendering
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatToolCallMessage(toolCall: ToolCall): {
|
|
6
|
+
toolName: string;
|
|
7
|
+
args: Array<{
|
|
8
|
+
key: string;
|
|
9
|
+
value: string;
|
|
10
|
+
isLast: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format tool call display information for UI rendering
|
|
3
|
+
*/
|
|
4
|
+
export function formatToolCallMessage(toolCall) {
|
|
5
|
+
try {
|
|
6
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
7
|
+
const argEntries = Object.entries(args);
|
|
8
|
+
const formattedArgs = [];
|
|
9
|
+
if (argEntries.length > 0) {
|
|
10
|
+
argEntries.forEach(([key, value], idx, arr) => {
|
|
11
|
+
const valueStr = typeof value === 'string'
|
|
12
|
+
? value.length > 60 ? `"${value.slice(0, 60)}..."` : `"${value}"`
|
|
13
|
+
: JSON.stringify(value);
|
|
14
|
+
formattedArgs.push({
|
|
15
|
+
key,
|
|
16
|
+
value: valueStr,
|
|
17
|
+
isLast: idx === arr.length - 1
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
toolName: toolCall.function.name,
|
|
23
|
+
args: formattedArgs
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
return {
|
|
28
|
+
toolName: toolCall.function.name,
|
|
29
|
+
args: []
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ChatMessage } from '../api/chat.js';
|
|
2
|
+
import type { Message } from '../ui/components/MessageList.js';
|
|
3
|
+
/**
|
|
4
|
+
* Convert API format session messages to UI format messages
|
|
5
|
+
*/
|
|
6
|
+
export declare function convertSessionMessagesToUI(sessionMessages: ChatMessage[]): Message[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { formatToolCallMessage } from './messageFormatter.js';
|
|
2
|
+
/**
|
|
3
|
+
* Convert API format session messages to UI format messages
|
|
4
|
+
*/
|
|
5
|
+
export function convertSessionMessagesToUI(sessionMessages) {
|
|
6
|
+
const uiMessages = [];
|
|
7
|
+
for (const msg of sessionMessages) {
|
|
8
|
+
// Skip system messages
|
|
9
|
+
if (msg.role === 'system')
|
|
10
|
+
continue;
|
|
11
|
+
// Handle tool role messages (tool execution results)
|
|
12
|
+
if (msg.role === 'tool') {
|
|
13
|
+
const isError = msg.content.startsWith('Error:');
|
|
14
|
+
const statusIcon = isError ? '✗' : '✓';
|
|
15
|
+
const statusText = isError ? `\n └─ ${msg.content}` : '';
|
|
16
|
+
const toolName = msg.tool_call_id || 'unknown-tool';
|
|
17
|
+
uiMessages.push({
|
|
18
|
+
role: 'assistant',
|
|
19
|
+
content: `${statusIcon} ${toolName}${statusText}`,
|
|
20
|
+
streaming: false,
|
|
21
|
+
toolResult: !isError ? msg.content : undefined
|
|
22
|
+
});
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
// Handle user and assistant messages
|
|
26
|
+
const uiMessage = {
|
|
27
|
+
role: msg.role,
|
|
28
|
+
content: msg.content,
|
|
29
|
+
streaming: false,
|
|
30
|
+
images: msg.images
|
|
31
|
+
};
|
|
32
|
+
// If assistant message has tool_calls, expand to show each tool call
|
|
33
|
+
if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
|
|
34
|
+
for (const toolCall of msg.tool_calls) {
|
|
35
|
+
const toolDisplay = formatToolCallMessage(toolCall);
|
|
36
|
+
let toolArgs;
|
|
37
|
+
try {
|
|
38
|
+
toolArgs = JSON.parse(toolCall.function.arguments);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
toolArgs = {};
|
|
42
|
+
}
|
|
43
|
+
uiMessages.push({
|
|
44
|
+
role: 'assistant',
|
|
45
|
+
content: `⚡ ${toolDisplay.toolName}`,
|
|
46
|
+
streaming: false,
|
|
47
|
+
toolCall: {
|
|
48
|
+
name: toolCall.function.name,
|
|
49
|
+
arguments: toolArgs
|
|
50
|
+
},
|
|
51
|
+
toolDisplay
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// Add regular message directly
|
|
57
|
+
uiMessages.push(uiMessage);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return uiMessages;
|
|
61
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ChatMessage as APIChatMessage } from '../api/chat.js';
|
|
2
|
+
export interface ChatMessage extends APIChatMessage {
|
|
3
|
+
timestamp: number;
|
|
4
|
+
}
|
|
5
|
+
export interface Session {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
summary: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
updatedAt: number;
|
|
11
|
+
messages: ChatMessage[];
|
|
12
|
+
messageCount: number;
|
|
13
|
+
}
|
|
14
|
+
export interface SessionListItem {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
summary: string;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
updatedAt: number;
|
|
20
|
+
messageCount: number;
|
|
21
|
+
}
|
|
22
|
+
declare class SessionManager {
|
|
23
|
+
private readonly sessionsDir;
|
|
24
|
+
private currentSession;
|
|
25
|
+
constructor();
|
|
26
|
+
private ensureSessionsDir;
|
|
27
|
+
private getSessionPath;
|
|
28
|
+
createNewSession(): Promise<Session>;
|
|
29
|
+
saveSession(session: Session): Promise<void>;
|
|
30
|
+
loadSession(sessionId: string): Promise<Session | null>;
|
|
31
|
+
listSessions(): Promise<SessionListItem[]>;
|
|
32
|
+
addMessage(message: ChatMessage): Promise<void>;
|
|
33
|
+
getCurrentSession(): Session | null;
|
|
34
|
+
setCurrentSession(session: Session): void;
|
|
35
|
+
clearCurrentSession(): void;
|
|
36
|
+
deleteSession(sessionId: string): Promise<boolean>;
|
|
37
|
+
}
|
|
38
|
+
export declare const sessionManager: SessionManager;
|
|
39
|
+
export {};
|