nova-terminal-assistant 0.1.0

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 nova-terminal-assistant might be problematic. Click here for more details.

Files changed (192) hide show
  1. package/README.md +358 -0
  2. package/bin/nova +38 -0
  3. package/bin/nova.js +12 -0
  4. package/package.json +67 -0
  5. package/src/cli/commands/SmartCompletion.ts +458 -0
  6. package/src/cli/index.ts +5 -0
  7. package/src/cli/startup/IFlowRepl.ts +212 -0
  8. package/src/cli/startup/InkBasedRepl.ts +1056 -0
  9. package/src/cli/startup/InteractiveRepl.ts +2833 -0
  10. package/src/cli/startup/NovaApp.ts +1861 -0
  11. package/src/cli/startup/index.ts +4 -0
  12. package/src/cli/startup/parseArgs.ts +293 -0
  13. package/src/cli/test-modules.ts +27 -0
  14. package/src/cli/ui/IFlowDropdown.ts +425 -0
  15. package/src/cli/ui/ModernReplUI.ts +276 -0
  16. package/src/cli/ui/SimpleSelector2.ts +215 -0
  17. package/src/cli/ui/components/ConfirmDialog.ts +176 -0
  18. package/src/cli/ui/components/ErrorPanel.ts +364 -0
  19. package/src/cli/ui/components/InkAppRunner.tsx +67 -0
  20. package/src/cli/ui/components/InkComponents.tsx +613 -0
  21. package/src/cli/ui/components/NovaInkApp.tsx +312 -0
  22. package/src/cli/ui/components/ProgressBar.ts +177 -0
  23. package/src/cli/ui/components/ProgressIndicator.ts +298 -0
  24. package/src/cli/ui/components/QuickActions.ts +396 -0
  25. package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
  26. package/src/cli/ui/components/StatusBar.ts +194 -0
  27. package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
  28. package/src/cli/ui/components/index.ts +27 -0
  29. package/src/cli/ui/ink-prototype.tsx +347 -0
  30. package/src/cli/utils/CliUI.ts +336 -0
  31. package/src/cli/utils/CompletionHelper.ts +388 -0
  32. package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
  33. package/src/cli/utils/EnhancedCompleter.ts +513 -0
  34. package/src/cli/utils/ErrorEnhancer.ts +429 -0
  35. package/src/cli/utils/OutputFormatter.ts +193 -0
  36. package/src/cli/utils/index.ts +9 -0
  37. package/src/core/agents/AgentOrchestrator.ts +515 -0
  38. package/src/core/agents/index.ts +17 -0
  39. package/src/core/audit/AuditLogger.ts +509 -0
  40. package/src/core/audit/index.ts +11 -0
  41. package/src/core/auth/AuthManager.d.ts.map +1 -0
  42. package/src/core/auth/AuthManager.ts +138 -0
  43. package/src/core/auth/index.d.ts.map +1 -0
  44. package/src/core/auth/index.ts +2 -0
  45. package/src/core/config/ConfigManager.d.ts.map +1 -0
  46. package/src/core/config/ConfigManager.test.ts +183 -0
  47. package/src/core/config/ConfigManager.ts +1219 -0
  48. package/src/core/config/index.d.ts.map +1 -0
  49. package/src/core/config/index.ts +1 -0
  50. package/src/core/context/ContextBuilder.d.ts.map +1 -0
  51. package/src/core/context/ContextBuilder.ts +171 -0
  52. package/src/core/context/ContextCompressor.d.ts.map +1 -0
  53. package/src/core/context/ContextCompressor.ts +642 -0
  54. package/src/core/context/LayeredMemoryManager.ts +657 -0
  55. package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
  56. package/src/core/context/MemoryDiscovery.ts +175 -0
  57. package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
  58. package/src/core/context/defaultSystemPrompt.ts +35 -0
  59. package/src/core/context/index.d.ts.map +1 -0
  60. package/src/core/context/index.ts +22 -0
  61. package/src/core/extensions/SkillGenerator.ts +421 -0
  62. package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
  63. package/src/core/extensions/SkillInstaller.ts +257 -0
  64. package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
  65. package/src/core/extensions/SkillRegistry.ts +361 -0
  66. package/src/core/extensions/SkillValidator.ts +525 -0
  67. package/src/core/extensions/index.ts +15 -0
  68. package/src/core/index.d.ts.map +1 -0
  69. package/src/core/index.ts +42 -0
  70. package/src/core/mcp/McpManager.d.ts.map +1 -0
  71. package/src/core/mcp/McpManager.ts +632 -0
  72. package/src/core/mcp/index.d.ts.map +1 -0
  73. package/src/core/mcp/index.ts +2 -0
  74. package/src/core/model/ModelClient.d.ts.map +1 -0
  75. package/src/core/model/ModelClient.ts +217 -0
  76. package/src/core/model/ModelConnectionTester.ts +363 -0
  77. package/src/core/model/ModelValidator.ts +348 -0
  78. package/src/core/model/index.d.ts.map +1 -0
  79. package/src/core/model/index.ts +6 -0
  80. package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
  81. package/src/core/model/providers/AnthropicProvider.ts +279 -0
  82. package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
  83. package/src/core/model/providers/CodingPlanProvider.ts +210 -0
  84. package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
  85. package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
  86. package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
  87. package/src/core/model/providers/OllamaManager.ts +201 -0
  88. package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
  89. package/src/core/model/providers/OllamaProvider.ts +73 -0
  90. package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  91. package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
  92. package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
  93. package/src/core/model/providers/OpenAIProvider.ts +29 -0
  94. package/src/core/model/providers/index.d.ts.map +1 -0
  95. package/src/core/model/providers/index.ts +12 -0
  96. package/src/core/model/types.d.ts.map +1 -0
  97. package/src/core/model/types.ts +77 -0
  98. package/src/core/security/ApprovalManager.d.ts.map +1 -0
  99. package/src/core/security/ApprovalManager.ts +174 -0
  100. package/src/core/security/FileFilter.d.ts.map +1 -0
  101. package/src/core/security/FileFilter.ts +141 -0
  102. package/src/core/security/HookExecutor.d.ts.map +1 -0
  103. package/src/core/security/HookExecutor.ts +178 -0
  104. package/src/core/security/SandboxExecutor.ts +447 -0
  105. package/src/core/security/index.d.ts.map +1 -0
  106. package/src/core/security/index.ts +8 -0
  107. package/src/core/session/AgentLoop.d.ts.map +1 -0
  108. package/src/core/session/AgentLoop.ts +501 -0
  109. package/src/core/session/SessionManager.d.ts.map +1 -0
  110. package/src/core/session/SessionManager.test.ts +183 -0
  111. package/src/core/session/SessionManager.ts +460 -0
  112. package/src/core/session/index.d.ts.map +1 -0
  113. package/src/core/session/index.ts +3 -0
  114. package/src/core/telemetry/Telemetry.d.ts.map +1 -0
  115. package/src/core/telemetry/Telemetry.ts +90 -0
  116. package/src/core/telemetry/TelemetryService.ts +531 -0
  117. package/src/core/telemetry/index.d.ts.map +1 -0
  118. package/src/core/telemetry/index.ts +12 -0
  119. package/src/core/testing/AutoFixer.ts +385 -0
  120. package/src/core/testing/ErrorAnalyzer.ts +499 -0
  121. package/src/core/testing/TestRunner.ts +265 -0
  122. package/src/core/testing/agent-cli-tests.ts +538 -0
  123. package/src/core/testing/index.ts +11 -0
  124. package/src/core/tools/ToolRegistry.d.ts.map +1 -0
  125. package/src/core/tools/ToolRegistry.test.ts +206 -0
  126. package/src/core/tools/ToolRegistry.ts +260 -0
  127. package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
  128. package/src/core/tools/impl/EditFileTool.ts +97 -0
  129. package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
  130. package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
  131. package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
  132. package/src/core/tools/impl/MemoryTool.ts +102 -0
  133. package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
  134. package/src/core/tools/impl/ReadFileTool.ts +58 -0
  135. package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
  136. package/src/core/tools/impl/SearchContentTool.ts +94 -0
  137. package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
  138. package/src/core/tools/impl/SearchFileTool.ts +61 -0
  139. package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
  140. package/src/core/tools/impl/ShellTool.ts +118 -0
  141. package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
  142. package/src/core/tools/impl/TaskTool.ts +207 -0
  143. package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
  144. package/src/core/tools/impl/TodoTool.ts +122 -0
  145. package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
  146. package/src/core/tools/impl/WebFetchTool.ts +103 -0
  147. package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
  148. package/src/core/tools/impl/WebSearchTool.ts +89 -0
  149. package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
  150. package/src/core/tools/impl/WriteFileTool.ts +49 -0
  151. package/src/core/tools/impl/index.d.ts.map +1 -0
  152. package/src/core/tools/impl/index.ts +16 -0
  153. package/src/core/tools/index.d.ts.map +1 -0
  154. package/src/core/tools/index.ts +7 -0
  155. package/src/core/tools/schemas/execution.d.ts.map +1 -0
  156. package/src/core/tools/schemas/execution.ts +42 -0
  157. package/src/core/tools/schemas/file.d.ts.map +1 -0
  158. package/src/core/tools/schemas/file.ts +119 -0
  159. package/src/core/tools/schemas/index.d.ts.map +1 -0
  160. package/src/core/tools/schemas/index.ts +11 -0
  161. package/src/core/tools/schemas/memory.d.ts.map +1 -0
  162. package/src/core/tools/schemas/memory.ts +52 -0
  163. package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
  164. package/src/core/tools/schemas/orchestration.ts +44 -0
  165. package/src/core/tools/schemas/search.d.ts.map +1 -0
  166. package/src/core/tools/schemas/search.ts +112 -0
  167. package/src/core/tools/schemas/todo.d.ts.map +1 -0
  168. package/src/core/tools/schemas/todo.ts +32 -0
  169. package/src/core/tools/schemas/web.d.ts.map +1 -0
  170. package/src/core/tools/schemas/web.ts +86 -0
  171. package/src/core/types/config.d.ts.map +1 -0
  172. package/src/core/types/config.ts +200 -0
  173. package/src/core/types/errors.d.ts.map +1 -0
  174. package/src/core/types/errors.ts +204 -0
  175. package/src/core/types/index.d.ts.map +1 -0
  176. package/src/core/types/index.ts +8 -0
  177. package/src/core/types/session.d.ts.map +1 -0
  178. package/src/core/types/session.ts +216 -0
  179. package/src/core/types/tools.d.ts.map +1 -0
  180. package/src/core/types/tools.ts +157 -0
  181. package/src/core/utils/CheckpointManager.d.ts.map +1 -0
  182. package/src/core/utils/CheckpointManager.ts +327 -0
  183. package/src/core/utils/Logger.d.ts.map +1 -0
  184. package/src/core/utils/Logger.ts +98 -0
  185. package/src/core/utils/RetryManager.ts +471 -0
  186. package/src/core/utils/TokenCounter.d.ts.map +1 -0
  187. package/src/core/utils/TokenCounter.ts +414 -0
  188. package/src/core/utils/VectorMemoryStore.ts +440 -0
  189. package/src/core/utils/helpers.d.ts.map +1 -0
  190. package/src/core/utils/helpers.ts +89 -0
  191. package/src/core/utils/index.d.ts.map +1 -0
  192. package/src/core/utils/index.ts +19 -0
@@ -0,0 +1,632 @@
1
+ // ============================================================================
2
+ // MCPManager v2 - Enhanced Model Context Protocol server management
3
+ // Adds: namespace isolation, notification handling, reconnection, resource support
4
+ // ============================================================================
5
+
6
+ import { spawn, type ChildProcess } from 'node:child_process';
7
+ import { EventEmitter } from 'node:events';
8
+ import type { ToolDefinition } from '../types/tools.js';
9
+ import { McpError } from '../types/errors.js';
10
+
11
+ export interface McpServerConfig {
12
+ name: string;
13
+ command: string;
14
+ args?: string[];
15
+ env?: Record<string, string>;
16
+ enabled?: boolean;
17
+ timeout?: number;
18
+ /** Transport type: stdio (default) or http/sse (future) */
19
+ transport?: 'stdio' | 'http' | 'sse';
20
+ /** HTTP URL for http/sse transport */
21
+ url?: string;
22
+ /** Headers for HTTP transport */
23
+ headers?: Record<string, string>;
24
+ /** Auto-reconnect on disconnect */
25
+ autoReconnect?: boolean;
26
+ /** Reconnect interval in ms (default: 5000) */
27
+ reconnectInterval?: number;
28
+ /** Maximum reconnect attempts (default: 3) */
29
+ maxReconnectAttempts?: number;
30
+ }
31
+
32
+ export interface McpToolDefinition {
33
+ name: string;
34
+ description: string;
35
+ inputSchema: Record<string, unknown>;
36
+ }
37
+
38
+ export interface McpResource {
39
+ uri: string;
40
+ name: string;
41
+ description?: string;
42
+ mimeType?: string;
43
+ }
44
+
45
+ export interface McpNotification {
46
+ method: string;
47
+ params?: Record<string, unknown>;
48
+ }
49
+
50
+ export interface McpServerStatus {
51
+ name: string;
52
+ toolCount: number;
53
+ resourceCount: number;
54
+ connected: boolean;
55
+ reconnectAttempts: number;
56
+ lastError?: string;
57
+ lastActivity: number;
58
+ }
59
+
60
+ type JsonRpcMessage = {
61
+ jsonrpc: '2.0';
62
+ id?: number;
63
+ method?: string;
64
+ params?: unknown;
65
+ result?: unknown;
66
+ error?: { code: number; message: string; data?: unknown };
67
+ };
68
+
69
+ // --- MCP Client with namespace isolation ---
70
+
71
+ interface McpClientEntry {
72
+ process: ChildProcess;
73
+ tools: McpToolDefinition[];
74
+ resources: McpResource[];
75
+ connected: boolean;
76
+ requestId: number;
77
+ pendingRequests: Map<number, {
78
+ resolve: (value: unknown) => void;
79
+ reject: (error: Error) => void;
80
+ timer: ReturnType<typeof setTimeout>;
81
+ }>;
82
+ buffer: string;
83
+ reconnectAttempts: number;
84
+ lastActivity: number;
85
+ lastError?: string;
86
+ }
87
+
88
+ // --- Enhanced McpManager ---
89
+
90
+ export class McpManager extends EventEmitter {
91
+ private servers = new Map<string, McpClientEntry>();
92
+ private configs = new Map<string, McpServerConfig>();
93
+ private toolServerMap = new Map<string, string>(); // toolName -> serverName
94
+
95
+ /** Connect to an MCP server */
96
+ async connect(config: McpServerConfig): Promise<ToolDefinition[]> {
97
+ if (this.servers.has(config.name)) {
98
+ const existing = this.servers.get(config.name)!;
99
+ if (existing.connected) {
100
+ return this.getToolsForServer(config.name);
101
+ }
102
+ // Server exists but disconnected, try reconnect
103
+ await this.disconnect(config.name);
104
+ }
105
+
106
+ // Validate config
107
+ if (!config.command && config.transport !== 'http' && config.transport !== 'sse') {
108
+ throw new McpError(`MCP server "${config.name}": command is required for stdio transport`, config.name);
109
+ }
110
+
111
+ this.configs.set(config.name, {
112
+ transport: 'stdio',
113
+ autoReconnect: true,
114
+ reconnectInterval: 5000,
115
+ maxReconnectAttempts: 3,
116
+ ...config,
117
+ });
118
+
119
+ // Connect with spawn error handling
120
+ let processRef: any = null;
121
+ try {
122
+ const entry = await this.createClient(config);
123
+ processRef = entry.process;
124
+ this.servers.set(config.name, entry);
125
+
126
+ // Set up event handler for auto-reconnect on exit
127
+ entry.process.on('exit', () => {
128
+ this.handleDisconnect(config.name);
129
+ });
130
+ // Set up error handler for runtime process errors
131
+ entry.process.on('error', (err: Error) => {
132
+ this.handleProcessError(config.name, err);
133
+ });
134
+
135
+ const tools = this.getToolsForServer(config.name);
136
+ this.emit('connected', { serverName: config.name, toolCount: tools.length });
137
+ return tools;
138
+ } catch (err) {
139
+ // If process was spawned but init failed, clean up
140
+ if (processRef) {
141
+ try { processRef.kill(); } catch {}
142
+ }
143
+ throw new McpError(
144
+ `Failed to connect to MCP server "${config.name}": ${(err as Error).message}`,
145
+ config.name
146
+ );
147
+ }
148
+ }
149
+
150
+ /** Call a tool on a specific MCP server */
151
+ async callTool(serverName: string, toolName: string, input: Record<string, unknown>): Promise<string> {
152
+ const entry = this.servers.get(serverName);
153
+ if (!entry) {
154
+ throw new McpError(`MCP server "${serverName}" not found`, serverName);
155
+ }
156
+ if (!entry.connected) {
157
+ throw new McpError(`MCP server "${serverName}" is not connected`, serverName);
158
+ }
159
+
160
+ try {
161
+ const result = await this.sendJsonRpc(entry, 'tools/call', {
162
+ name: toolName,
163
+ arguments: input,
164
+ });
165
+ entry.lastActivity = Date.now();
166
+ return JSON.stringify(result);
167
+ } catch (err) {
168
+ throw new McpError(
169
+ `MCP tool call failed on "${serverName}": ${(err as Error).message}`,
170
+ serverName
171
+ );
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Call a tool by its namespaced name (e.g., "filesystem:read_file").
177
+ * Automatically resolves the server.
178
+ */
179
+ async callToolByNamespacedName(namespacedName: string, input: Record<string, unknown>): Promise<string> {
180
+ const colonIdx = namespacedName.indexOf(':');
181
+ if (colonIdx < 0) {
182
+ throw new McpError(`Invalid namespaced tool name "${namespacedName}", expected format "server:tool"`, 'unknown');
183
+ }
184
+
185
+ const serverName = namespacedName.slice(0, colonIdx);
186
+ const toolName = namespacedName.slice(colonIdx + 1);
187
+
188
+ // Check tool-server map first
189
+ const mappedServer = this.toolServerMap.get(namespacedName);
190
+ if (mappedServer) {
191
+ return this.callTool(mappedServer, toolName, input);
192
+ }
193
+
194
+ // Fallback: try the server name extracted from the namespace
195
+ return this.callTool(serverName, toolName, input);
196
+ }
197
+
198
+ /** Read a resource from an MCP server */
199
+ async readResource(serverName: string, uri: string): Promise<{ contents: Array<{ uri: string; mimeType?: string; text?: string; blob?: string }> }> {
200
+ const entry = this.servers.get(serverName);
201
+ if (!entry || !entry.connected) {
202
+ throw new McpError(`MCP server "${serverName}" not connected`, serverName);
203
+ }
204
+
205
+ const result = await this.sendJsonRpc(entry, 'resources/read', { uri });
206
+ entry.lastActivity = Date.now();
207
+ return result as any;
208
+ }
209
+
210
+ /** List available resources from an MCP server */
211
+ async listResources(serverName: string): Promise<McpResource[]> {
212
+ const entry = this.servers.get(serverName);
213
+ if (!entry || !entry.connected) {
214
+ return [];
215
+ }
216
+
217
+ try {
218
+ const result = await this.sendJsonRpc(entry, 'resources/list', {});
219
+ entry.resources = (result as any)?.resources || [];
220
+ return entry.resources;
221
+ } catch {
222
+ return [];
223
+ }
224
+ }
225
+
226
+ /** Subscribe to resource changes */
227
+ async subscribeResource(serverName: string, uri: string): Promise<void> {
228
+ const entry = this.servers.get(serverName);
229
+ if (!entry || !entry.connected) {
230
+ throw new McpError(`MCP server "${serverName}" not connected`, serverName);
231
+ }
232
+
233
+ await this.sendJsonRpc(entry, 'resources/subscribe', { uri });
234
+ entry.lastActivity = Date.now();
235
+ }
236
+
237
+ /** Disconnect from an MCP server */
238
+ async disconnect(serverName: string): Promise<void> {
239
+ const entry = this.servers.get(serverName);
240
+ if (entry) {
241
+ try {
242
+ // Try graceful shutdown via notification
243
+ if (entry.process.stdin && !entry.process.stdin.destroyed) {
244
+ entry.process.stdin.write(JSON.stringify({
245
+ jsonrpc: '2.0',
246
+ method: 'notifications/cancelled',
247
+ params: {},
248
+ }) + '\n');
249
+ }
250
+ } catch {
251
+ // Ignore write errors during shutdown
252
+ }
253
+
254
+ // Clean up pending requests
255
+ for (const [, pending] of entry.pendingRequests) {
256
+ clearTimeout(pending.timer);
257
+ pending.reject(new Error('Server disconnected'));
258
+ }
259
+
260
+ try {
261
+ entry.process.kill();
262
+ } catch {
263
+ // Process may have already exited
264
+ }
265
+ this.servers.delete(serverName);
266
+ }
267
+ }
268
+
269
+ /** Disconnect from all servers */
270
+ async disconnectAll(): Promise<void> {
271
+ const names = Array.from(this.servers.keys());
272
+ await Promise.all(names.map((name) => this.disconnect(name)));
273
+ }
274
+
275
+ /** List connected servers with status */
276
+ listServers(): McpServerStatus[] {
277
+ return Array.from(this.servers.entries()).map(([name, entry]) => ({
278
+ name,
279
+ toolCount: entry.tools.length,
280
+ resourceCount: entry.resources.length,
281
+ connected: entry.connected,
282
+ reconnectAttempts: entry.reconnectAttempts,
283
+ lastError: entry.lastError,
284
+ lastActivity: entry.lastActivity,
285
+ }));
286
+ }
287
+
288
+ /**
289
+ * Get all tools from all connected servers, with namespace isolation.
290
+ * Tool names are prefixed with "server:" to avoid conflicts.
291
+ */
292
+ getAllTools(): ToolDefinition[] {
293
+ const allTools: ToolDefinition[] = [];
294
+
295
+ for (const [serverName, entry] of this.servers) {
296
+ if (!entry.connected) continue;
297
+
298
+ for (const mcpTool of entry.tools) {
299
+ const namespacedName = `${serverName}:${mcpTool.name}`;
300
+ const tool: ToolDefinition = {
301
+ name: namespacedName,
302
+ description: `[${serverName}] ${mcpTool.description}`,
303
+ category: 'mcp',
304
+ inputSchema: mcpTool.inputSchema,
305
+ requiresApproval: true,
306
+ riskLevel: 'medium',
307
+ tags: ['mcp', serverName],
308
+ };
309
+ allTools.push(tool);
310
+ this.toolServerMap.set(namespacedName, serverName);
311
+ }
312
+ }
313
+
314
+ return allTools;
315
+ }
316
+
317
+ /** Get tools for a specific server (without namespace prefix) */
318
+ getToolsForServer(serverName: string): ToolDefinition[] {
319
+ const entry = this.servers.get(serverName);
320
+ if (!entry) return [];
321
+
322
+ return entry.tools.map((mcpTool) => ({
323
+ name: mcpTool.name,
324
+ description: mcpTool.description,
325
+ category: 'mcp' as const,
326
+ inputSchema: mcpTool.inputSchema,
327
+ requiresApproval: true,
328
+ riskLevel: 'medium' as const,
329
+ tags: ['mcp', serverName],
330
+ }));
331
+ }
332
+
333
+ // ========================================================================
334
+ // Private: Client Lifecycle
335
+ // ========================================================================
336
+
337
+ private async createClient(config: McpServerConfig): Promise<McpClientEntry> {
338
+ let proc: any;
339
+ try {
340
+ proc = spawn(config.command, config.args || [], {
341
+ env: { ...process.env, ...config.env },
342
+ stdio: ['pipe', 'pipe', 'pipe'],
343
+ windowsHide: true,
344
+ });
345
+ } catch (err) {
346
+ throw new McpError(
347
+ `Failed to spawn MCP server "${config.name}": ${(err as Error).message}`,
348
+ config.name
349
+ );
350
+ }
351
+
352
+ const entry: McpClientEntry = {
353
+ process: proc,
354
+ tools: [],
355
+ resources: [],
356
+ connected: false,
357
+ requestId: 0,
358
+ pendingRequests: new Map(),
359
+ buffer: '',
360
+ reconnectAttempts: 0,
361
+ lastActivity: Date.now(),
362
+ };
363
+
364
+ // Set up stdout handler
365
+ proc.stdout?.on('data', (data: Buffer) => {
366
+ this.handleStdoutData(entry, data);
367
+ });
368
+
369
+ proc.stderr?.on('data', (data: Buffer) => {
370
+ // MCP servers may log to stderr
371
+ const text = data.toString().trim();
372
+ if (text) {
373
+ this.emit('stderr', { serverName: config.name, data: text });
374
+ }
375
+ });
376
+
377
+ // Initialize the connection - use Promise.race to handle async spawn errors (e.g., ENOENT)
378
+ const timeout = config.timeout || 15000;
379
+ const initPromise = this.initializeConnection(entry, timeout);
380
+ const spawnErrorPromise = new Promise<never>((_, reject) => {
381
+ proc.on('error', (err: Error) => {
382
+ reject(new McpError(
383
+ `Failed to spawn MCP server "${config.name}": ${err.message}`,
384
+ config.name
385
+ ));
386
+ });
387
+ });
388
+
389
+ await Promise.race([initPromise, spawnErrorPromise]);
390
+
391
+ entry.connected = true;
392
+ return entry;
393
+ }
394
+
395
+ private async initializeConnection(entry: McpClientEntry, timeout: number): Promise<void> {
396
+ return new Promise((resolve, reject) => {
397
+ const timer = setTimeout(() => {
398
+ entry.process.kill();
399
+ reject(new McpError('MCP server initialization timed out'));
400
+ }, timeout);
401
+
402
+ let initReceived = false;
403
+ let toolsReceived = false;
404
+
405
+ const completionHandler = () => {
406
+ if (initReceived && toolsReceived) {
407
+ clearTimeout(timer);
408
+ resolve();
409
+ }
410
+ };
411
+
412
+ // Override stdout handler during initialization
413
+ const originalHandler = entry.process.stdout?.listeners('data');
414
+ // Remove existing listeners
415
+ for (const listener of (originalHandler || [])) {
416
+ entry.process.stdout?.removeListener('data', listener as (...args: any[]) => void);
417
+ }
418
+
419
+ entry.process.stdout?.on('data', (data: Buffer) => {
420
+ entry.buffer += data.toString();
421
+ const lines = entry.buffer.split('\n');
422
+ entry.buffer = lines.pop() || '';
423
+
424
+ for (const line of lines) {
425
+ if (!line.trim()) continue;
426
+ try {
427
+ const response: JsonRpcMessage = JSON.parse(line);
428
+
429
+ if (!initReceived && response.id === 1 && response.result) {
430
+ initReceived = true;
431
+
432
+ // Send tools/list request
433
+ const toolsRequest: JsonRpcMessage = {
434
+ jsonrpc: '2.0',
435
+ id: 2,
436
+ method: 'tools/list',
437
+ params: {},
438
+ };
439
+ entry.process.stdin?.write(JSON.stringify(toolsRequest) + '\n');
440
+ completionHandler();
441
+ } else if (!toolsReceived && response.id === 2 && response.result) {
442
+ toolsReceived = true;
443
+ entry.tools = (response.result as any)?.tools || [];
444
+ completionHandler();
445
+
446
+ // Also request resources/list
447
+ const resourcesRequest: JsonRpcMessage = {
448
+ jsonrpc: '2.0',
449
+ id: 3,
450
+ method: 'resources/list',
451
+ params: {},
452
+ };
453
+ entry.process.stdin?.write(JSON.stringify(resourcesRequest) + '\n');
454
+ } else if (response.id === 3 && response.result) {
455
+ entry.resources = (response.result as any)?.resources || [];
456
+
457
+ // Restore normal data handler
458
+ for (const listener of (originalHandler || [])) {
459
+ entry.process.stdout?.on('data', listener as (...args: any[]) => void);
460
+ }
461
+ // Remove our temp listener
462
+ entry.process.stdout?.removeAllListeners('data');
463
+ for (const listener of (originalHandler || [])) {
464
+ entry.process.stdout?.on('data', listener as (...args: any[]) => void);
465
+ }
466
+ } else if (response.id && entry.pendingRequests.has(response.id)) {
467
+ // Route to pending request handler
468
+ const pending = entry.pendingRequests.get(response.id)!;
469
+ clearTimeout(pending.timer);
470
+ entry.pendingRequests.delete(response.id);
471
+
472
+ if (response.error) {
473
+ pending.reject(new Error(response.error.message));
474
+ } else {
475
+ pending.resolve(response.result);
476
+ }
477
+ } else if (!response.id && response.method) {
478
+ // This is a notification from the server
479
+ this.handleNotification(entry, response);
480
+ }
481
+ } catch {
482
+ // Ignore unparseable lines
483
+ }
484
+ }
485
+ });
486
+
487
+ // Send initialize request
488
+ const initRequest: JsonRpcMessage = {
489
+ jsonrpc: '2.0',
490
+ id: 1,
491
+ method: 'initialize',
492
+ params: {
493
+ protocolVersion: '2024-11-05',
494
+ capabilities: {
495
+ tools: {},
496
+ resources: { subscribe: true },
497
+ prompts: {},
498
+ },
499
+ clientInfo: { name: 'nova-cli', version: '0.2.0' },
500
+ },
501
+ };
502
+ entry.process.stdin?.write(JSON.stringify(initRequest) + '\n');
503
+ });
504
+ }
505
+
506
+ private handleStdoutData(entry: McpClientEntry, data: Buffer): void {
507
+ entry.buffer += data.toString();
508
+ const lines = entry.buffer.split('\n');
509
+ entry.buffer = lines.pop() || '';
510
+
511
+ for (const line of lines) {
512
+ if (!line.trim()) continue;
513
+ try {
514
+ const response: JsonRpcMessage = JSON.parse(line);
515
+
516
+ if (response.id && entry.pendingRequests.has(response.id)) {
517
+ const pending = entry.pendingRequests.get(response.id)!;
518
+ clearTimeout(pending.timer);
519
+ entry.pendingRequests.delete(response.id);
520
+
521
+ if (response.error) {
522
+ pending.reject(new Error(response.error.message));
523
+ } else {
524
+ pending.resolve(response.result);
525
+ }
526
+ } else if (!response.id) {
527
+ // Notification from server
528
+ this.handleNotification(entry, response);
529
+ }
530
+ } catch {
531
+ // Ignore unparseable lines
532
+ }
533
+ }
534
+ }
535
+
536
+ private handleNotification(entry: McpClientEntry, notification: JsonRpcMessage): void {
537
+ const method = notification.method || '';
538
+ entry.lastActivity = Date.now();
539
+
540
+ // Handle resource update notifications
541
+ if (method === 'notifications/resources/updated') {
542
+ const uri = (notification.params as any)?.uri;
543
+ this.emit('resource-updated', { serverName: '', uri }); // Would need serverName from entry
544
+ // Refresh resources list
545
+ this.listResources('').catch(() => {}); // Best effort
546
+ }
547
+
548
+ // Emit generic notification event
549
+ this.emit('notification', {
550
+ method,
551
+ params: notification.params,
552
+ });
553
+ }
554
+
555
+ private handleDisconnect(serverName: string): void {
556
+ const entry = this.servers.get(serverName);
557
+ if (!entry) return;
558
+
559
+ entry.connected = false;
560
+ this.emit('disconnected', { serverName });
561
+
562
+ // Try auto-reconnect
563
+ const config = this.configs.get(serverName);
564
+ if (config?.autoReconnect && entry.reconnectAttempts < (config.maxReconnectAttempts || 3)) {
565
+ entry.reconnectAttempts++;
566
+ entry.lastError = `Process exited unexpectedly (attempt ${entry.reconnectAttempts})`;
567
+
568
+ const interval = config.reconnectInterval || 5000;
569
+ this.emit('reconnecting', {
570
+ serverName,
571
+ attempt: entry.reconnectAttempts,
572
+ maxAttempts: config.maxReconnectAttempts || 3,
573
+ });
574
+
575
+ setTimeout(async () => {
576
+ try {
577
+ await this.disconnect(serverName);
578
+ const newEntry = await this.createClient(config);
579
+ newEntry.reconnectAttempts = entry.reconnectAttempts;
580
+ this.servers.set(serverName, newEntry);
581
+
582
+ newEntry.process.on('exit', () => this.handleDisconnect(serverName));
583
+ newEntry.process.on('error', (err) => this.handleProcessError(serverName, err));
584
+
585
+ this.emit('reconnected', { serverName, toolCount: newEntry.tools.length });
586
+ } catch (err) {
587
+ this.emit('reconnect-failed', {
588
+ serverName,
589
+ error: (err as Error).message,
590
+ attempt: entry.reconnectAttempts,
591
+ });
592
+ }
593
+ }, interval);
594
+ }
595
+ }
596
+
597
+ private handleProcessError(serverName: string, err: Error): void {
598
+ const entry = this.servers.get(serverName);
599
+ if (entry) {
600
+ entry.lastError = err.message;
601
+ }
602
+ // Use 'serverError' instead of 'error' to avoid EventEmitter unhandled error crash
603
+ this.emit('serverError', { serverName, error: err.message });
604
+ }
605
+
606
+ // ========================================================================
607
+ // Private: JSON-RPC
608
+ // ========================================================================
609
+
610
+ private sendJsonRpc(entry: McpClientEntry, method: string, params: unknown): Promise<unknown> {
611
+ return new Promise((resolve, reject) => {
612
+ const id = ++entry.requestId;
613
+ const request: JsonRpcMessage = { jsonrpc: '2.0', id, method, params };
614
+
615
+ const timer = setTimeout(() => {
616
+ entry.pendingRequests.delete(id);
617
+ reject(new Error(`MCP request "${method}" timed out after 30s`));
618
+ }, 30000);
619
+
620
+ entry.pendingRequests.set(id, { resolve, reject, timer });
621
+
622
+ if (!entry.process.stdin || entry.process.stdin.destroyed) {
623
+ clearTimeout(timer);
624
+ entry.pendingRequests.delete(id);
625
+ reject(new Error('MCP server process stdin is closed'));
626
+ return;
627
+ }
628
+
629
+ entry.process.stdin.write(JSON.stringify(request) + '\n');
630
+ });
631
+ }
632
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { McpManager } from './McpManager.js';
2
+ // McpServerConfig and McpToolDefinition are exported from types/config.js
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModelClient.d.ts","sourceRoot":"","sources":["ModelClient.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,OAAO,EAKP,SAAS,EACV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAMxD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAChG,OAAO,KAAK,EAAiB,mBAAmB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEjG,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,cAAc,GAAG,aAAa,GAAG,QAAQ,CAAC;IAClG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAS;gBAEhB,OAAO,EAAE,wBAAwB;IA8F7C,8CAA8C;IACxC,QAAQ,CACZ,QAAQ,EAAE,OAAO,EAAE,EACnB,KAAK,EAAE,cAAc,EAAE,EACvB,SAAS,EAAE,SAAS,EACpB,OAAO,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,GACrC,OAAO,CAAC,aAAa,CAAC;IAwBzB,0CAA0C;IACnC,MAAM,CACX,QAAQ,EAAE,OAAO,EAAE,EACnB,KAAK,EAAE,cAAc,EAAE,EACvB,SAAS,EAAE,SAAS,EACpB,OAAO,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,GACrC,cAAc,CAAC,WAAW,CAAC;IAyB9B,qDAAqD;IAC/C,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAIvD,iCAAiC;IACjC,QAAQ,IAAI,MAAM;IAIlB,4BAA4B;IAC5B,eAAe,IAAI,MAAM;IAIzB,8BAA8B;IAC9B,aAAa,CAAC,OAAO,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;CAK3F"}