@build-astron-co/nimbus 0.2.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.
Files changed (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +628 -0
  3. package/bin/nimbus +38 -0
  4. package/package.json +80 -0
  5. package/src/__tests__/app.test.ts +76 -0
  6. package/src/__tests__/audit.test.ts +877 -0
  7. package/src/__tests__/circuit-breaker.test.ts +116 -0
  8. package/src/__tests__/cli-run.test.ts +115 -0
  9. package/src/__tests__/context-manager.test.ts +502 -0
  10. package/src/__tests__/context.test.ts +242 -0
  11. package/src/__tests__/enterprise.test.ts +401 -0
  12. package/src/__tests__/generator.test.ts +433 -0
  13. package/src/__tests__/hooks.test.ts +582 -0
  14. package/src/__tests__/init.test.ts +436 -0
  15. package/src/__tests__/intent-parser.test.ts +229 -0
  16. package/src/__tests__/llm-router.test.ts +209 -0
  17. package/src/__tests__/lsp.test.ts +293 -0
  18. package/src/__tests__/modes.test.ts +336 -0
  19. package/src/__tests__/permissions.test.ts +338 -0
  20. package/src/__tests__/serve.test.ts +275 -0
  21. package/src/__tests__/sessions.test.ts +227 -0
  22. package/src/__tests__/sharing.test.ts +288 -0
  23. package/src/__tests__/snapshots.test.ts +581 -0
  24. package/src/__tests__/state-db.test.ts +334 -0
  25. package/src/__tests__/stream-with-tools.test.ts +732 -0
  26. package/src/__tests__/subagents.test.ts +176 -0
  27. package/src/__tests__/system-prompt.test.ts +169 -0
  28. package/src/__tests__/tool-converter.test.ts +256 -0
  29. package/src/__tests__/tool-schemas.test.ts +397 -0
  30. package/src/__tests__/tools.test.ts +143 -0
  31. package/src/__tests__/version.test.ts +49 -0
  32. package/src/agent/compaction-agent.ts +227 -0
  33. package/src/agent/context-manager.ts +435 -0
  34. package/src/agent/context.ts +427 -0
  35. package/src/agent/deploy-preview.ts +426 -0
  36. package/src/agent/index.ts +68 -0
  37. package/src/agent/loop.ts +717 -0
  38. package/src/agent/modes.ts +429 -0
  39. package/src/agent/permissions.ts +466 -0
  40. package/src/agent/subagents/base.ts +116 -0
  41. package/src/agent/subagents/cost.ts +51 -0
  42. package/src/agent/subagents/explore.ts +42 -0
  43. package/src/agent/subagents/general.ts +54 -0
  44. package/src/agent/subagents/index.ts +102 -0
  45. package/src/agent/subagents/infra.ts +59 -0
  46. package/src/agent/subagents/security.ts +69 -0
  47. package/src/agent/system-prompt.ts +436 -0
  48. package/src/app.ts +122 -0
  49. package/src/audit/activity-log.ts +290 -0
  50. package/src/audit/compliance-checker.ts +540 -0
  51. package/src/audit/cost-tracker.ts +318 -0
  52. package/src/audit/index.ts +23 -0
  53. package/src/audit/security-scanner.ts +596 -0
  54. package/src/auth/guard.ts +75 -0
  55. package/src/auth/index.ts +56 -0
  56. package/src/auth/oauth.ts +455 -0
  57. package/src/auth/providers.ts +470 -0
  58. package/src/auth/sso.ts +113 -0
  59. package/src/auth/store.ts +505 -0
  60. package/src/auth/types.ts +187 -0
  61. package/src/build.ts +141 -0
  62. package/src/cli/index.ts +16 -0
  63. package/src/cli/init.ts +854 -0
  64. package/src/cli/openapi-spec.ts +356 -0
  65. package/src/cli/run.ts +237 -0
  66. package/src/cli/serve-auth.ts +80 -0
  67. package/src/cli/serve.ts +462 -0
  68. package/src/cli/web.ts +67 -0
  69. package/src/cli.ts +1417 -0
  70. package/src/clients/core-engine-client.ts +227 -0
  71. package/src/clients/enterprise-client.ts +334 -0
  72. package/src/clients/generator-client.ts +351 -0
  73. package/src/clients/git-client.ts +627 -0
  74. package/src/clients/github-client.ts +410 -0
  75. package/src/clients/helm-client.ts +504 -0
  76. package/src/clients/index.ts +80 -0
  77. package/src/clients/k8s-client.ts +497 -0
  78. package/src/clients/llm-client.ts +161 -0
  79. package/src/clients/rest-client.ts +130 -0
  80. package/src/clients/service-discovery.ts +33 -0
  81. package/src/clients/terraform-client.ts +482 -0
  82. package/src/clients/tools-client.ts +1843 -0
  83. package/src/clients/ws-client.ts +115 -0
  84. package/src/commands/analyze/index.ts +352 -0
  85. package/src/commands/apply/helm.ts +473 -0
  86. package/src/commands/apply/index.ts +213 -0
  87. package/src/commands/apply/k8s.ts +454 -0
  88. package/src/commands/apply/terraform.ts +582 -0
  89. package/src/commands/ask.ts +167 -0
  90. package/src/commands/audit/index.ts +238 -0
  91. package/src/commands/auth-cloud.ts +294 -0
  92. package/src/commands/auth-list.ts +134 -0
  93. package/src/commands/auth-profile.ts +121 -0
  94. package/src/commands/auth-status.ts +141 -0
  95. package/src/commands/aws/ec2.ts +501 -0
  96. package/src/commands/aws/iam.ts +397 -0
  97. package/src/commands/aws/index.ts +133 -0
  98. package/src/commands/aws/lambda.ts +396 -0
  99. package/src/commands/aws/rds.ts +439 -0
  100. package/src/commands/aws/s3.ts +439 -0
  101. package/src/commands/aws/vpc.ts +393 -0
  102. package/src/commands/aws-discover.ts +649 -0
  103. package/src/commands/aws-terraform.ts +805 -0
  104. package/src/commands/azure/aks.ts +376 -0
  105. package/src/commands/azure/functions.ts +253 -0
  106. package/src/commands/azure/index.ts +116 -0
  107. package/src/commands/azure/storage.ts +478 -0
  108. package/src/commands/azure/vm.ts +355 -0
  109. package/src/commands/billing/index.ts +256 -0
  110. package/src/commands/chat.ts +314 -0
  111. package/src/commands/config.ts +346 -0
  112. package/src/commands/cost/cloud-cost-estimator.ts +266 -0
  113. package/src/commands/cost/estimator.ts +79 -0
  114. package/src/commands/cost/index.ts +594 -0
  115. package/src/commands/cost/parsers/terraform.ts +273 -0
  116. package/src/commands/cost/parsers/types.ts +25 -0
  117. package/src/commands/cost/pricing/aws.ts +544 -0
  118. package/src/commands/cost/pricing/azure.ts +499 -0
  119. package/src/commands/cost/pricing/gcp.ts +396 -0
  120. package/src/commands/cost/pricing/index.ts +40 -0
  121. package/src/commands/demo.ts +250 -0
  122. package/src/commands/doctor.ts +794 -0
  123. package/src/commands/drift/index.ts +439 -0
  124. package/src/commands/explain.ts +277 -0
  125. package/src/commands/feedback.ts +389 -0
  126. package/src/commands/fix.ts +324 -0
  127. package/src/commands/fs/index.ts +402 -0
  128. package/src/commands/gcp/compute.ts +325 -0
  129. package/src/commands/gcp/functions.ts +271 -0
  130. package/src/commands/gcp/gke.ts +438 -0
  131. package/src/commands/gcp/iam.ts +344 -0
  132. package/src/commands/gcp/index.ts +129 -0
  133. package/src/commands/gcp/storage.ts +284 -0
  134. package/src/commands/generate-helm.ts +1249 -0
  135. package/src/commands/generate-k8s.ts +1560 -0
  136. package/src/commands/generate-terraform.ts +1460 -0
  137. package/src/commands/gh/index.ts +863 -0
  138. package/src/commands/git/index.ts +1343 -0
  139. package/src/commands/helm/index.ts +1126 -0
  140. package/src/commands/help.ts +539 -0
  141. package/src/commands/history.ts +142 -0
  142. package/src/commands/import.ts +868 -0
  143. package/src/commands/index.ts +367 -0
  144. package/src/commands/init.ts +1046 -0
  145. package/src/commands/k8s/index.ts +1137 -0
  146. package/src/commands/login.ts +631 -0
  147. package/src/commands/logout.ts +83 -0
  148. package/src/commands/onboarding.ts +228 -0
  149. package/src/commands/plan/display.ts +279 -0
  150. package/src/commands/plan/index.ts +599 -0
  151. package/src/commands/preview.ts +452 -0
  152. package/src/commands/questionnaire.ts +1270 -0
  153. package/src/commands/resume.ts +55 -0
  154. package/src/commands/team/index.ts +346 -0
  155. package/src/commands/template.ts +232 -0
  156. package/src/commands/tf/index.ts +1034 -0
  157. package/src/commands/upgrade.ts +550 -0
  158. package/src/commands/usage/index.ts +134 -0
  159. package/src/commands/version.ts +170 -0
  160. package/src/compat/index.ts +2 -0
  161. package/src/compat/runtime.ts +12 -0
  162. package/src/compat/sqlite.ts +107 -0
  163. package/src/config/index.ts +17 -0
  164. package/src/config/manager.ts +530 -0
  165. package/src/config/safety-policy.ts +358 -0
  166. package/src/config/schema.ts +125 -0
  167. package/src/config/types.ts +527 -0
  168. package/src/context/context-db.ts +199 -0
  169. package/src/demo/index.ts +349 -0
  170. package/src/demo/scenarios/full-journey.ts +229 -0
  171. package/src/demo/scenarios/getting-started.ts +127 -0
  172. package/src/demo/scenarios/helm-release.ts +341 -0
  173. package/src/demo/scenarios/k8s-deployment.ts +194 -0
  174. package/src/demo/scenarios/terraform-vpc.ts +170 -0
  175. package/src/demo/types.ts +92 -0
  176. package/src/engine/cost-estimator.ts +438 -0
  177. package/src/engine/diagram-generator.ts +256 -0
  178. package/src/engine/drift-detector.ts +902 -0
  179. package/src/engine/executor.ts +1035 -0
  180. package/src/engine/index.ts +76 -0
  181. package/src/engine/orchestrator.ts +636 -0
  182. package/src/engine/planner.ts +720 -0
  183. package/src/engine/safety.ts +743 -0
  184. package/src/engine/verifier.ts +770 -0
  185. package/src/enterprise/audit.ts +348 -0
  186. package/src/enterprise/auth.ts +270 -0
  187. package/src/enterprise/billing.ts +822 -0
  188. package/src/enterprise/index.ts +17 -0
  189. package/src/enterprise/teams.ts +443 -0
  190. package/src/generator/best-practices.ts +1608 -0
  191. package/src/generator/helm.ts +630 -0
  192. package/src/generator/index.ts +37 -0
  193. package/src/generator/intent-parser.ts +514 -0
  194. package/src/generator/kubernetes.ts +976 -0
  195. package/src/generator/terraform.ts +1867 -0
  196. package/src/history/index.ts +8 -0
  197. package/src/history/manager.ts +322 -0
  198. package/src/history/types.ts +34 -0
  199. package/src/hooks/config.ts +432 -0
  200. package/src/hooks/engine.ts +391 -0
  201. package/src/hooks/index.ts +4 -0
  202. package/src/llm/auth-bridge.ts +198 -0
  203. package/src/llm/circuit-breaker.ts +140 -0
  204. package/src/llm/config-loader.ts +201 -0
  205. package/src/llm/cost-calculator.ts +171 -0
  206. package/src/llm/index.ts +8 -0
  207. package/src/llm/model-aliases.ts +115 -0
  208. package/src/llm/provider-registry.ts +63 -0
  209. package/src/llm/providers/anthropic.ts +433 -0
  210. package/src/llm/providers/bedrock.ts +477 -0
  211. package/src/llm/providers/google.ts +405 -0
  212. package/src/llm/providers/ollama.ts +767 -0
  213. package/src/llm/providers/openai-compatible.ts +340 -0
  214. package/src/llm/providers/openai.ts +328 -0
  215. package/src/llm/providers/openrouter.ts +338 -0
  216. package/src/llm/router.ts +1035 -0
  217. package/src/llm/types.ts +232 -0
  218. package/src/lsp/client.ts +298 -0
  219. package/src/lsp/languages.ts +116 -0
  220. package/src/lsp/manager.ts +278 -0
  221. package/src/mcp/client.ts +402 -0
  222. package/src/mcp/index.ts +5 -0
  223. package/src/mcp/manager.ts +133 -0
  224. package/src/nimbus.ts +214 -0
  225. package/src/plugins/index.ts +27 -0
  226. package/src/plugins/loader.ts +334 -0
  227. package/src/plugins/manager.ts +376 -0
  228. package/src/plugins/types.ts +284 -0
  229. package/src/scanners/cicd-scanner.ts +258 -0
  230. package/src/scanners/cloud-scanner.ts +466 -0
  231. package/src/scanners/framework-scanner.ts +469 -0
  232. package/src/scanners/iac-scanner.ts +388 -0
  233. package/src/scanners/index.ts +539 -0
  234. package/src/scanners/language-scanner.ts +276 -0
  235. package/src/scanners/package-manager-scanner.ts +277 -0
  236. package/src/scanners/types.ts +172 -0
  237. package/src/sessions/manager.ts +365 -0
  238. package/src/sessions/types.ts +44 -0
  239. package/src/sharing/sync.ts +296 -0
  240. package/src/sharing/viewer.ts +97 -0
  241. package/src/snapshots/index.ts +2 -0
  242. package/src/snapshots/manager.ts +530 -0
  243. package/src/state/artifacts.ts +147 -0
  244. package/src/state/audit.ts +137 -0
  245. package/src/state/billing.ts +240 -0
  246. package/src/state/checkpoints.ts +117 -0
  247. package/src/state/config.ts +67 -0
  248. package/src/state/conversations.ts +14 -0
  249. package/src/state/credentials.ts +154 -0
  250. package/src/state/db.ts +58 -0
  251. package/src/state/index.ts +26 -0
  252. package/src/state/messages.ts +115 -0
  253. package/src/state/projects.ts +123 -0
  254. package/src/state/schema.ts +236 -0
  255. package/src/state/sessions.ts +147 -0
  256. package/src/state/teams.ts +200 -0
  257. package/src/telemetry.ts +108 -0
  258. package/src/tools/aws-ops.ts +952 -0
  259. package/src/tools/azure-ops.ts +579 -0
  260. package/src/tools/file-ops.ts +593 -0
  261. package/src/tools/gcp-ops.ts +625 -0
  262. package/src/tools/git-ops.ts +773 -0
  263. package/src/tools/github-ops.ts +799 -0
  264. package/src/tools/helm-ops.ts +943 -0
  265. package/src/tools/index.ts +17 -0
  266. package/src/tools/k8s-ops.ts +819 -0
  267. package/src/tools/schemas/converter.ts +184 -0
  268. package/src/tools/schemas/devops.ts +612 -0
  269. package/src/tools/schemas/index.ts +73 -0
  270. package/src/tools/schemas/standard.ts +1144 -0
  271. package/src/tools/schemas/types.ts +705 -0
  272. package/src/tools/terraform-ops.ts +862 -0
  273. package/src/types/ambient.d.ts +193 -0
  274. package/src/types/config.ts +83 -0
  275. package/src/types/drift.ts +116 -0
  276. package/src/types/enterprise.ts +335 -0
  277. package/src/types/index.ts +20 -0
  278. package/src/types/plan.ts +44 -0
  279. package/src/types/request.ts +65 -0
  280. package/src/types/response.ts +54 -0
  281. package/src/types/service.ts +51 -0
  282. package/src/ui/App.tsx +997 -0
  283. package/src/ui/DeployPreview.tsx +169 -0
  284. package/src/ui/Header.tsx +68 -0
  285. package/src/ui/InputBox.tsx +350 -0
  286. package/src/ui/MessageList.tsx +585 -0
  287. package/src/ui/PermissionPrompt.tsx +151 -0
  288. package/src/ui/StatusBar.tsx +158 -0
  289. package/src/ui/ToolCallDisplay.tsx +409 -0
  290. package/src/ui/chat-ui.ts +853 -0
  291. package/src/ui/index.ts +33 -0
  292. package/src/ui/ink/index.ts +711 -0
  293. package/src/ui/streaming.ts +176 -0
  294. package/src/ui/types.ts +57 -0
  295. package/src/utils/analytics.ts +72 -0
  296. package/src/utils/cost-warning.ts +27 -0
  297. package/src/utils/env.ts +46 -0
  298. package/src/utils/errors.ts +69 -0
  299. package/src/utils/event-bus.ts +38 -0
  300. package/src/utils/index.ts +24 -0
  301. package/src/utils/logger.ts +171 -0
  302. package/src/utils/rate-limiter.ts +121 -0
  303. package/src/utils/service-auth.ts +49 -0
  304. package/src/utils/validation.ts +53 -0
  305. package/src/version.ts +4 -0
  306. package/src/watcher/index.ts +163 -0
  307. package/src/wizard/approval.ts +383 -0
  308. package/src/wizard/index.ts +25 -0
  309. package/src/wizard/prompts.ts +338 -0
  310. package/src/wizard/types.ts +171 -0
  311. package/src/wizard/ui.ts +556 -0
  312. package/src/wizard/wizard.ts +304 -0
  313. package/tsconfig.json +24 -0
@@ -0,0 +1,278 @@
1
+ /**
2
+ * LSP Manager -- Language Server Lifecycle
3
+ *
4
+ * Manages starting, stopping, and querying multiple language servers.
5
+ * Lazy loading: servers only start when a file of that type is first edited.
6
+ * Auto-stop: servers shut down after 5 minutes of inactivity.
7
+ */
8
+
9
+ import { readFile } from 'node:fs/promises';
10
+ import { exec } from 'node:child_process';
11
+ import { promisify } from 'node:util';
12
+ import { LSPClient, type Diagnostic } from './client';
13
+ import { getLanguageForFile, LANGUAGE_CONFIGS, type LanguageConfig } from './languages';
14
+
15
+ const execAsync = promisify(exec);
16
+
17
+ /** Default idle timeout: 5 minutes. */
18
+ const DEFAULT_IDLE_TIMEOUT = 5 * 60 * 1000;
19
+
20
+ /** Delay after file change before requesting diagnostics. */
21
+ const DIAGNOSTIC_DELAY = 500;
22
+
23
+ export interface LSPStatus {
24
+ language: string;
25
+ active: boolean;
26
+ available: boolean;
27
+ }
28
+
29
+ export class LSPManager {
30
+ private clients = new Map<string, LSPClient>();
31
+ private idleTimers = new Map<string, Timer>();
32
+ private rootUri: string;
33
+ private availabilityCache = new Map<string, boolean>();
34
+ private fileVersions = new Map<string, number>();
35
+ private enabled: boolean = true;
36
+
37
+ constructor(rootUri?: string) {
38
+ this.rootUri = rootUri ?? process.cwd();
39
+ }
40
+
41
+ /** Enable or disable LSP integration. */
42
+ setEnabled(enabled: boolean): void {
43
+ this.enabled = enabled;
44
+ if (!enabled) {
45
+ this.stopAll();
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Notify the LSP manager that a file was modified.
51
+ * This lazily starts the appropriate language server and sends the update.
52
+ */
53
+ async touchFile(filePath: string): Promise<void> {
54
+ if (!this.enabled) {
55
+ return;
56
+ }
57
+
58
+ const config = getLanguageForFile(filePath);
59
+ if (!config) {
60
+ return;
61
+ }
62
+
63
+ // Ensure the client is running
64
+ const client = await this.ensureClient(config);
65
+ if (!client) {
66
+ return;
67
+ }
68
+
69
+ // Read file content and bump version
70
+ let content: string;
71
+ try {
72
+ content = await readFile(filePath, 'utf-8');
73
+ } catch {
74
+ return;
75
+ }
76
+
77
+ const version = (this.fileVersions.get(filePath) ?? 0) + 1;
78
+ this.fileVersions.set(filePath, version);
79
+
80
+ await client.touchFile(filePath, content, version);
81
+ this.resetIdleTimer(config.id);
82
+ }
83
+
84
+ /**
85
+ * Get diagnostics for a file.
86
+ * Waits up to `delayMs` for the LSP to process and return results.
87
+ */
88
+ async getDiagnostics(
89
+ filePath: string,
90
+ delayMs: number = DIAGNOSTIC_DELAY
91
+ ): Promise<Diagnostic[]> {
92
+ if (!this.enabled) {
93
+ return [];
94
+ }
95
+
96
+ const config = getLanguageForFile(filePath);
97
+ if (!config) {
98
+ return [];
99
+ }
100
+
101
+ const client = this.clients.get(config.id);
102
+ if (!client?.isInitialized) {
103
+ return [];
104
+ }
105
+
106
+ // Wait a brief moment for the LSP to process
107
+ await new Promise(resolve => setTimeout(resolve, delayMs));
108
+
109
+ return client.getDiagnostics(filePath);
110
+ }
111
+
112
+ /**
113
+ * Get errors only (severity 1 = Error).
114
+ */
115
+ async getErrors(filePath: string): Promise<Diagnostic[]> {
116
+ const diagnostics = await this.getDiagnostics(filePath);
117
+ return diagnostics.filter(d => d.severity === 1);
118
+ }
119
+
120
+ /**
121
+ * Format diagnostics as messages suitable for injection into the agent conversation.
122
+ */
123
+ formatDiagnosticsForAgent(diagnostics: Diagnostic[]): string | null {
124
+ if (diagnostics.length === 0) {
125
+ return null;
126
+ }
127
+
128
+ const errors = diagnostics.filter(d => d.severity === 1);
129
+ const warnings = diagnostics.filter(d => d.severity === 2);
130
+
131
+ if (errors.length === 0 && warnings.length === 0) {
132
+ return null;
133
+ }
134
+
135
+ const lines: string[] = ['[LSP Diagnostics]'];
136
+ for (const d of errors) {
137
+ const loc = `${d.file}:${d.line}:${d.column}`;
138
+ lines.push(` Error: ${loc} — ${d.message}${d.source ? ` (${d.source})` : ''}`);
139
+ }
140
+ for (const d of warnings.slice(0, 5)) {
141
+ const loc = `${d.file}:${d.line}:${d.column}`;
142
+ lines.push(` Warning: ${loc} — ${d.message}${d.source ? ` (${d.source})` : ''}`);
143
+ }
144
+
145
+ if (warnings.length > 5) {
146
+ lines.push(` ... and ${warnings.length - 5} more warnings`);
147
+ }
148
+
149
+ return lines.join('\n');
150
+ }
151
+
152
+ /** Get status of all known language servers. */
153
+ async getStatus(): Promise<LSPStatus[]> {
154
+ const statuses: LSPStatus[] = [];
155
+ for (const config of LANGUAGE_CONFIGS) {
156
+ const client = this.clients.get(config.id);
157
+ const available = await this.isAvailable(config);
158
+ statuses.push({
159
+ language: config.name,
160
+ active: client?.isInitialized ?? false,
161
+ available,
162
+ });
163
+ }
164
+ return statuses;
165
+ }
166
+
167
+ /** Stop all running language servers. */
168
+ async stopAll(): Promise<void> {
169
+ for (const [id, client] of this.clients) {
170
+ await client.stop();
171
+ const timer = this.idleTimers.get(id);
172
+ if (timer) {
173
+ clearTimeout(timer);
174
+ }
175
+ }
176
+ this.clients.clear();
177
+ this.idleTimers.clear();
178
+ }
179
+
180
+ /** Stop a specific language server. */
181
+ async stop(languageId: string): Promise<void> {
182
+ const client = this.clients.get(languageId);
183
+ if (client) {
184
+ await client.stop();
185
+ this.clients.delete(languageId);
186
+ }
187
+ const timer = this.idleTimers.get(languageId);
188
+ if (timer) {
189
+ clearTimeout(timer);
190
+ this.idleTimers.delete(languageId);
191
+ }
192
+ }
193
+
194
+ /** Ensure a client exists and is initialized for the given language. */
195
+ private async ensureClient(config: LanguageConfig): Promise<LSPClient | null> {
196
+ const existing = this.clients.get(config.id);
197
+ if (existing?.isInitialized) {
198
+ return existing;
199
+ }
200
+
201
+ // Check if the binary is available
202
+ const available = await this.isAvailable(config);
203
+ if (!available) {
204
+ return null;
205
+ }
206
+
207
+ const client = new LSPClient(config, this.rootUri);
208
+ const started = await client.start();
209
+ if (!started) {
210
+ return null;
211
+ }
212
+
213
+ this.clients.set(config.id, client);
214
+ this.resetIdleTimer(config.id);
215
+
216
+ client.on('exit', () => {
217
+ this.clients.delete(config.id);
218
+ const timer = this.idleTimers.get(config.id);
219
+ if (timer) {
220
+ clearTimeout(timer);
221
+ this.idleTimers.delete(config.id);
222
+ }
223
+ });
224
+
225
+ return client;
226
+ }
227
+
228
+ /** Check if a language server binary is available in PATH. */
229
+ private async isAvailable(config: LanguageConfig): Promise<boolean> {
230
+ const cached = this.availabilityCache.get(config.id);
231
+ if (cached !== undefined) {
232
+ return cached;
233
+ }
234
+
235
+ try {
236
+ await execAsync(`which ${config.command}`);
237
+ this.availabilityCache.set(config.id, true);
238
+ return true;
239
+ } catch {
240
+ this.availabilityCache.set(config.id, false);
241
+ return false;
242
+ }
243
+ }
244
+
245
+ /** Reset the idle timer for a language server. */
246
+ private resetIdleTimer(languageId: string): void {
247
+ const existing = this.idleTimers.get(languageId);
248
+ if (existing) {
249
+ clearTimeout(existing);
250
+ }
251
+
252
+ const config = LANGUAGE_CONFIGS.find(c => c.id === languageId);
253
+ const timeout = config?.idleTimeout ?? DEFAULT_IDLE_TIMEOUT;
254
+
255
+ const timer = setTimeout(() => {
256
+ this.stop(languageId);
257
+ }, timeout);
258
+
259
+ this.idleTimers.set(languageId, timer);
260
+ }
261
+ }
262
+
263
+ /** Singleton LSP manager instance. */
264
+ let lspManagerInstance: LSPManager | null = null;
265
+
266
+ /** Get or create the singleton LSP manager. */
267
+ export function getLSPManager(rootUri?: string): LSPManager {
268
+ if (!lspManagerInstance) {
269
+ lspManagerInstance = new LSPManager(rootUri);
270
+ }
271
+ return lspManagerInstance;
272
+ }
273
+
274
+ /** Reset the singleton (for testing). */
275
+ export function resetLSPManager(): void {
276
+ lspManagerInstance?.stopAll();
277
+ lspManagerInstance = null;
278
+ }
@@ -0,0 +1,402 @@
1
+ /**
2
+ * MCP Client
3
+ *
4
+ * Connects to Model Context Protocol servers (local command-based or
5
+ * remote HTTP-based) and dynamically registers their tools into
6
+ * the Nimbus tool registry.
7
+ *
8
+ * MCP specification: https://modelcontextprotocol.io/
9
+ */
10
+
11
+ import { spawn, type ChildProcess } from 'node:child_process';
12
+ import type { ToolDefinition, ToolResult } from '../tools/schemas/types';
13
+ import { z } from 'zod';
14
+
15
+ /** MCP server configuration */
16
+ export interface MCPServerConfig {
17
+ /** Unique server identifier */
18
+ name: string;
19
+ /** Server type */
20
+ type: 'command' | 'http';
21
+ /** For command servers: the command to spawn */
22
+ command?: string;
23
+ /** For command servers: command arguments */
24
+ args?: string[];
25
+ /** For command servers: environment variables */
26
+ env?: Record<string, string>;
27
+ /** For HTTP servers: the base URL */
28
+ url?: string;
29
+ /** For HTTP servers: authentication token */
30
+ token?: string;
31
+ /** Whether to connect lazily (only when a tool is used) */
32
+ lazy?: boolean;
33
+ }
34
+
35
+ /** MCP tool definition from server */
36
+ interface MCPToolDefinition {
37
+ name: string;
38
+ description: string;
39
+ inputSchema: JSONSchemaProperty;
40
+ }
41
+
42
+ /** MCP JSON-RPC message */
43
+ interface JSONRPCMessage {
44
+ jsonrpc: '2.0';
45
+ id?: number | string;
46
+ method?: string;
47
+ params?: unknown;
48
+ result?: unknown;
49
+ error?: { code: number; message: string; data?: unknown };
50
+ }
51
+
52
+ /** Shape of MCP content blocks returned by tools/call */
53
+ interface MCPContentBlock {
54
+ type: string;
55
+ text?: string;
56
+ }
57
+
58
+ /** Shape of MCP tools/call result */
59
+ interface MCPCallResult {
60
+ content?: MCPContentBlock[];
61
+ isError?: boolean;
62
+ }
63
+
64
+ /**
65
+ * MCP Client that connects to a single MCP server.
66
+ */
67
+ export class MCPClient {
68
+ readonly config: MCPServerConfig;
69
+ private process: ChildProcess | null = null;
70
+ private connected = false;
71
+ private requestId = 0;
72
+ private pendingRequests = new Map<
73
+ number,
74
+ {
75
+ resolve: (value: unknown) => void;
76
+ reject: (error: Error) => void;
77
+ }
78
+ >();
79
+ private buffer = '';
80
+ private tools: MCPToolDefinition[] = [];
81
+
82
+ constructor(config: MCPServerConfig) {
83
+ this.config = config;
84
+ }
85
+
86
+ /** Whether the client is connected to the server */
87
+ get isConnected(): boolean {
88
+ return this.connected;
89
+ }
90
+
91
+ /** The tools discovered from this server */
92
+ get discoveredTools(): readonly MCPToolDefinition[] {
93
+ return this.tools;
94
+ }
95
+
96
+ /**
97
+ * Connect to the MCP server.
98
+ * For command servers, spawns the process and initializes via JSON-RPC.
99
+ * For HTTP servers, sends a GET to check availability.
100
+ */
101
+ async connect(): Promise<void> {
102
+ if (this.connected) {
103
+ return;
104
+ }
105
+
106
+ if (this.config.type === 'command') {
107
+ await this.connectCommand();
108
+ } else {
109
+ await this.connectHttp();
110
+ }
111
+
112
+ this.connected = true;
113
+ }
114
+
115
+ /**
116
+ * Discover tools from the connected server.
117
+ */
118
+ async listTools(): Promise<MCPToolDefinition[]> {
119
+ if (!this.connected) {
120
+ await this.connect();
121
+ }
122
+
123
+ if (this.config.type === 'command') {
124
+ const response = (await this.sendRequest('tools/list', {})) as {
125
+ tools?: MCPToolDefinition[];
126
+ };
127
+ this.tools = response.tools ?? [];
128
+ } else {
129
+ // HTTP server
130
+ const headers: Record<string, string> = {};
131
+ if (this.config.token) {
132
+ headers['Authorization'] = `Bearer ${this.config.token}`;
133
+ }
134
+ const response = await fetch(`${this.config.url}/tools/list`, { headers });
135
+ const data = (await response.json()) as { tools?: MCPToolDefinition[] };
136
+ this.tools = data.tools ?? [];
137
+ }
138
+
139
+ return this.tools;
140
+ }
141
+
142
+ /**
143
+ * Call a tool on the MCP server.
144
+ */
145
+ async callTool(name: string, input: unknown): Promise<ToolResult> {
146
+ if (!this.connected) {
147
+ await this.connect();
148
+ }
149
+
150
+ try {
151
+ let result: MCPCallResult;
152
+
153
+ if (this.config.type === 'command') {
154
+ result = (await this.sendRequest('tools/call', {
155
+ name,
156
+ arguments: input,
157
+ })) as MCPCallResult;
158
+ } else {
159
+ const headers: Record<string, string> = {
160
+ 'Content-Type': 'application/json',
161
+ };
162
+ if (this.config.token) {
163
+ headers['Authorization'] = `Bearer ${this.config.token}`;
164
+ }
165
+ const response = await fetch(`${this.config.url}/tools/call`, {
166
+ method: 'POST',
167
+ headers,
168
+ body: JSON.stringify({ name, arguments: input }),
169
+ });
170
+ result = (await response.json()) as MCPCallResult;
171
+ }
172
+
173
+ // MCP tool results have content array
174
+ const content = result.content ?? [];
175
+ const textParts = content
176
+ .filter(
177
+ (c): c is MCPContentBlock & { text: string } =>
178
+ c.type === 'text' && typeof c.text === 'string'
179
+ )
180
+ .map(c => c.text);
181
+
182
+ return {
183
+ output: textParts.join('\n') || JSON.stringify(result),
184
+ isError: result.isError ?? false,
185
+ error: result.isError ? textParts.join('\n') : undefined,
186
+ };
187
+ } catch (error: unknown) {
188
+ const msg = error instanceof Error ? error.message : String(error);
189
+ return {
190
+ output: '',
191
+ error: `MCP tool call failed: ${msg}`,
192
+ isError: true,
193
+ };
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Convert discovered MCP tools to Nimbus ToolDefinition format.
199
+ */
200
+ toToolDefinitions(): ToolDefinition[] {
201
+ return this.tools.map(mcpTool => ({
202
+ name: `mcp_${this.config.name}_${mcpTool.name}`,
203
+ description: `[MCP: ${this.config.name}] ${mcpTool.description}`,
204
+ inputSchema: jsonSchemaToZod(mcpTool.inputSchema),
205
+ execute: async (input: unknown) => this.callTool(mcpTool.name, input),
206
+ permissionTier: 'ask_once' as const,
207
+ category: 'mcp' as const,
208
+ }));
209
+ }
210
+
211
+ /**
212
+ * Disconnect from the server.
213
+ */
214
+ async disconnect(): Promise<void> {
215
+ if (this.process) {
216
+ this.process.kill();
217
+ this.process = null;
218
+ }
219
+ this.connected = false;
220
+ this.tools = [];
221
+ this.pendingRequests.clear();
222
+ }
223
+
224
+ // ---------------------------------------------------------------
225
+ // Internal: Command-based server
226
+ // ---------------------------------------------------------------
227
+
228
+ private async connectCommand(): Promise<void> {
229
+ if (!this.config.command) {
230
+ throw new Error(
231
+ `MCP server '${this.config.name}' has type 'command' but no command specified`
232
+ );
233
+ }
234
+
235
+ this.process = spawn(this.config.command, this.config.args ?? [], {
236
+ stdio: ['pipe', 'pipe', 'pipe'],
237
+ env: { ...process.env, ...this.config.env },
238
+ });
239
+
240
+ // Handle stdout (JSON-RPC responses)
241
+ this.process.stdout?.on('data', (data: Buffer) => {
242
+ this.buffer += data.toString();
243
+ this.processBuffer();
244
+ });
245
+
246
+ this.process.on('exit', () => {
247
+ this.connected = false;
248
+ });
249
+
250
+ // Send initialize request
251
+ await this.sendRequest('initialize', {
252
+ protocolVersion: '2024-11-05',
253
+ capabilities: {},
254
+ clientInfo: { name: 'nimbus', version: '0.2.0' },
255
+ });
256
+
257
+ // Send initialized notification
258
+ this.sendNotification('notifications/initialized', {});
259
+ }
260
+
261
+ private async connectHttp(): Promise<void> {
262
+ if (!this.config.url) {
263
+ throw new Error(`MCP server '${this.config.name}' has type 'http' but no URL specified`);
264
+ }
265
+
266
+ // Ping the server
267
+ const headers: Record<string, string> = {};
268
+ if (this.config.token) {
269
+ headers['Authorization'] = `Bearer ${this.config.token}`;
270
+ }
271
+ const response = await fetch(this.config.url, { headers });
272
+
273
+ if (!response.ok) {
274
+ throw new Error(`MCP server '${this.config.name}' returned HTTP ${response.status}`);
275
+ }
276
+ }
277
+
278
+ private sendRequest(method: string, params: unknown): Promise<unknown> {
279
+ return new Promise((resolve, reject) => {
280
+ const id = ++this.requestId;
281
+ this.pendingRequests.set(id, { resolve, reject });
282
+
283
+ const message: JSONRPCMessage = {
284
+ jsonrpc: '2.0',
285
+ id,
286
+ method,
287
+ params,
288
+ };
289
+
290
+ if (this.process?.stdin) {
291
+ this.process.stdin.write(`${JSON.stringify(message)}\n`);
292
+ } else {
293
+ reject(new Error('MCP server process stdin not available'));
294
+ }
295
+
296
+ // Timeout after 30 seconds
297
+ setTimeout(() => {
298
+ if (this.pendingRequests.has(id)) {
299
+ this.pendingRequests.delete(id);
300
+ reject(new Error(`MCP request timed out: ${method}`));
301
+ }
302
+ }, 30_000);
303
+ });
304
+ }
305
+
306
+ private sendNotification(method: string, params: unknown): void {
307
+ const message: JSONRPCMessage = {
308
+ jsonrpc: '2.0',
309
+ method,
310
+ params,
311
+ };
312
+
313
+ if (this.process?.stdin) {
314
+ this.process.stdin.write(`${JSON.stringify(message)}\n`);
315
+ }
316
+ }
317
+
318
+ private processBuffer(): void {
319
+ const lines = this.buffer.split('\n');
320
+ this.buffer = lines.pop() ?? '';
321
+
322
+ for (const line of lines) {
323
+ if (!line.trim()) {
324
+ continue;
325
+ }
326
+ try {
327
+ const message = JSON.parse(line) as JSONRPCMessage;
328
+ if (message.id !== undefined && this.pendingRequests.has(message.id as number)) {
329
+ const pending = this.pendingRequests.get(message.id as number)!;
330
+ this.pendingRequests.delete(message.id as number);
331
+ if (message.error) {
332
+ pending.reject(new Error(message.error.message));
333
+ } else {
334
+ pending.resolve(message.result);
335
+ }
336
+ }
337
+ } catch {
338
+ // Skip non-JSON lines
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+ // ---------------------------------------------------------------
345
+ // JSON Schema -> Zod conversion
346
+ // ---------------------------------------------------------------
347
+
348
+ /** Minimal representation of a JSON Schema property used during conversion */
349
+ interface JSONSchemaProperty {
350
+ type?: string;
351
+ description?: string;
352
+ enum?: [string, ...string[]];
353
+ items?: JSONSchemaProperty;
354
+ properties?: Record<string, JSONSchemaProperty>;
355
+ required?: string[];
356
+ }
357
+
358
+ /**
359
+ * Convert a JSON Schema object to a Zod schema.
360
+ * Handles basic types used in MCP tool definitions.
361
+ */
362
+ function jsonSchemaToZod(schema: JSONSchemaProperty | undefined): z.ZodType<unknown> {
363
+ if (!schema || schema.type !== 'object') {
364
+ return z.object({});
365
+ }
366
+
367
+ const shape: Record<string, z.ZodType<unknown>> = {};
368
+ const required = new Set(schema.required ?? []);
369
+
370
+ for (const [key, prop] of Object.entries(schema.properties ?? {})) {
371
+ let fieldSchema: z.ZodType<unknown>;
372
+
373
+ switch (prop.type) {
374
+ case 'string':
375
+ fieldSchema = prop.enum ? z.enum(prop.enum) : z.string();
376
+ break;
377
+ case 'number':
378
+ case 'integer':
379
+ fieldSchema = z.number();
380
+ break;
381
+ case 'boolean':
382
+ fieldSchema = z.boolean();
383
+ break;
384
+ case 'array':
385
+ fieldSchema = z.array(prop.items ? jsonSchemaToZod(prop.items) : z.unknown());
386
+ break;
387
+ case 'object':
388
+ fieldSchema = jsonSchemaToZod(prop);
389
+ break;
390
+ default:
391
+ fieldSchema = z.unknown();
392
+ }
393
+
394
+ if (prop.description) {
395
+ fieldSchema = fieldSchema.describe(prop.description);
396
+ }
397
+
398
+ shape[key] = required.has(key) ? fieldSchema : fieldSchema.optional();
399
+ }
400
+
401
+ return z.object(shape);
402
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * MCP (Model Context Protocol) -- Barrel re-exports
3
+ */
4
+ export { MCPClient, type MCPServerConfig } from './client';
5
+ export { MCPManager, type MCPConfig } from './manager';