@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,133 @@
1
+ /**
2
+ * MCP Manager
3
+ *
4
+ * Manages multiple MCP server connections and provides a unified
5
+ * interface for tool discovery and registration.
6
+ */
7
+
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import { homedir } from 'node:os';
11
+ import { MCPClient, type MCPServerConfig } from './client';
12
+ import type { ToolDefinition, ToolRegistry } from '../tools/schemas/types';
13
+
14
+ /** Configuration file format for MCP servers */
15
+ export interface MCPConfig {
16
+ mcpServers?: Record<string, Omit<MCPServerConfig, 'name'>>;
17
+ }
18
+
19
+ export class MCPManager {
20
+ private clients: Map<string, MCPClient> = new Map();
21
+ private initialized = false;
22
+
23
+ /**
24
+ * Load MCP server configurations from config files.
25
+ * Searches: .nimbus/mcp.json, nimbus.json, ~/.nimbus/mcp.json
26
+ */
27
+ async loadConfig(cwd?: string): Promise<void> {
28
+ const configPaths = [
29
+ cwd ? path.join(cwd, '.nimbus', 'mcp.json') : null,
30
+ cwd ? path.join(cwd, 'nimbus.json') : null,
31
+ path.join(homedir(), '.nimbus', 'mcp.json'),
32
+ ].filter((p): p is string => p !== null);
33
+
34
+ for (const configPath of configPaths) {
35
+ if (fs.existsSync(configPath)) {
36
+ try {
37
+ const content = fs.readFileSync(configPath, 'utf-8');
38
+ const config: MCPConfig = JSON.parse(content) as MCPConfig;
39
+ if (config.mcpServers) {
40
+ for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
41
+ if (!this.clients.has(name)) {
42
+ this.clients.set(name, new MCPClient({ ...serverConfig, name }));
43
+ }
44
+ }
45
+ }
46
+ } catch {
47
+ // Skip invalid config files
48
+ }
49
+ }
50
+ }
51
+
52
+ this.initialized = true;
53
+ }
54
+
55
+ /**
56
+ * Connect to all configured MCP servers and discover tools.
57
+ * Lazy servers are skipped until explicitly needed.
58
+ */
59
+ async connectAll(): Promise<void> {
60
+ if (!this.initialized) {
61
+ await this.loadConfig();
62
+ }
63
+
64
+ const connectPromises: Promise<void>[] = [];
65
+ for (const [name, client] of this.clients) {
66
+ if (!client.config.lazy) {
67
+ connectPromises.push(
68
+ client
69
+ .connect()
70
+ .then(() => client.listTools())
71
+ .then(() => undefined)
72
+ .catch((err: unknown) => {
73
+ const msg = err instanceof Error ? err.message : String(err);
74
+ console.warn(`MCP server '${name}' failed to connect: ${msg}`);
75
+ })
76
+ );
77
+ }
78
+ }
79
+
80
+ await Promise.all(connectPromises);
81
+ }
82
+
83
+ /**
84
+ * Get all tool definitions from connected MCP servers.
85
+ */
86
+ getAllTools(): ToolDefinition[] {
87
+ const tools: ToolDefinition[] = [];
88
+ for (const client of this.clients.values()) {
89
+ if (client.isConnected) {
90
+ tools.push(...client.toToolDefinitions());
91
+ }
92
+ }
93
+ return tools;
94
+ }
95
+
96
+ /**
97
+ * Register all MCP tools into a tool registry.
98
+ */
99
+ registerTools(registry: ToolRegistry): void {
100
+ for (const tool of this.getAllTools()) {
101
+ try {
102
+ registry.register(tool);
103
+ } catch {
104
+ // Skip duplicate tool names
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Get a specific client by server name.
111
+ */
112
+ getClient(name: string): MCPClient | undefined {
113
+ return this.clients.get(name);
114
+ }
115
+
116
+ /**
117
+ * Disconnect all MCP servers.
118
+ */
119
+ async disconnectAll(): Promise<void> {
120
+ const disconnectPromises = Array.from(this.clients.values()).map(client => client.disconnect());
121
+ await Promise.all(disconnectPromises);
122
+ }
123
+
124
+ /** Number of configured servers */
125
+ get serverCount(): number {
126
+ return this.clients.size;
127
+ }
128
+
129
+ /** Number of connected servers */
130
+ get connectedCount(): number {
131
+ return Array.from(this.clients.values()).filter(c => c.isConnected).length;
132
+ }
133
+ }
package/src/nimbus.ts ADDED
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Nimbus CLI — Main Entry Point
4
+ *
5
+ * Single-process embedded architecture. All modules (LLM, tools, state,
6
+ * enterprise, engine, generator) run in-process — no HTTP services needed.
7
+ *
8
+ * Usage:
9
+ * bun src/nimbus.ts --version
10
+ * bun src/nimbus.ts --help
11
+ * bun src/nimbus.ts chat
12
+ * bun src/nimbus.ts ask "what is terraform"
13
+ * bun src/nimbus.ts generate terraform --provider aws
14
+ * bun src/nimbus.ts tf plan
15
+ */
16
+
17
+ import { VERSION } from './version';
18
+
19
+ /**
20
+ * Non-blocking update check. Fires a single HTTPS HEAD request to npm
21
+ * and prints a one-liner to stderr if a newer version exists. The check
22
+ * uses a 3-second timeout so it never slows startup.
23
+ */
24
+ function checkForUpdates(): void {
25
+ // Only check for interactive TTY sessions, not in CI
26
+ if (!process.stderr.isTTY || process.env.CI || process.env.NIMBUS_NO_UPDATE_CHECK) {
27
+ return;
28
+ }
29
+
30
+ // Fire-and-forget — deferred to let the TUI render first
31
+ (async () => {
32
+ try {
33
+ // Small delay so the check doesn't compete with startup I/O
34
+ await new Promise(r => setTimeout(r, 500));
35
+ const controller = new AbortController();
36
+ const timeout = setTimeout(() => controller.abort(), 1500);
37
+
38
+ const res = await fetch('https://registry.npmjs.org/@build-astron-co/nimbus/latest', {
39
+ signal: controller.signal,
40
+ headers: { Accept: 'application/json' },
41
+ });
42
+ clearTimeout(timeout);
43
+
44
+ if (!res.ok) {
45
+ return;
46
+ }
47
+
48
+ const data = (await res.json()) as { version?: string };
49
+ const latest = data.version;
50
+ if (!latest || latest === VERSION) {
51
+ return;
52
+ }
53
+
54
+ // Simple semver comparison: split on dots, compare numerically
55
+ const current = VERSION.split('.').map(Number);
56
+ const remote = latest.split('.').map(Number);
57
+ let isNewer = false;
58
+ for (let i = 0; i < 3; i++) {
59
+ if ((remote[i] ?? 0) > (current[i] ?? 0)) {
60
+ isNewer = true;
61
+ break;
62
+ }
63
+ if ((remote[i] ?? 0) < (current[i] ?? 0)) {
64
+ break;
65
+ }
66
+ }
67
+
68
+ if (isNewer) {
69
+ process.stderr.write(
70
+ `\x1b[33m Update available: ${VERSION} → ${latest}. Run: nimbus upgrade\x1b[0m\n`
71
+ );
72
+ }
73
+ } catch {
74
+ // Network errors are silently ignored
75
+ }
76
+ })();
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Global error handlers (Gap 6: prevent silent crashes from unhandled promises)
81
+ // ---------------------------------------------------------------------------
82
+
83
+ process.on('unhandledRejection', reason => {
84
+ const msg = reason instanceof Error ? reason.message : String(reason);
85
+ process.stderr.write(`\x1b[31mUnhandled promise rejection: ${msg}\x1b[0m\n`);
86
+ // Don't exit — let the TUI continue running. The user can see the error
87
+ // and decide whether to continue or quit.
88
+ });
89
+
90
+ process.on('uncaughtException', error => {
91
+ process.stderr.write(`\x1b[31mUncaught exception: ${error.message}\x1b[0m\n`);
92
+ if (error.stack) {
93
+ process.stderr.write(`\x1b[2m${error.stack}\x1b[0m\n`);
94
+ }
95
+ // For uncaught exceptions, set exit code but let cleanup run
96
+ process.exitCode = 1;
97
+ });
98
+
99
+ async function main() {
100
+ const args = process.argv.slice(2);
101
+
102
+ // Handle --version and -v before anything else (no init needed)
103
+ if (args[0] === '--version' || args[0] === '-v') {
104
+ console.log(`nimbus ${VERSION}`);
105
+ process.exit(0);
106
+ }
107
+
108
+ // Show help when explicitly requested — handles both `nimbus --help`
109
+ // and subcommand-level help like `nimbus chat --help` or `nimbus tf -h`.
110
+ if (args.includes('--help') || args.includes('-h')) {
111
+ const otherArgs = args.filter(a => a !== '--help' && a !== '-h');
112
+ args.length = 0;
113
+ args.push('help', ...otherArgs);
114
+ }
115
+
116
+ // Kick off a non-blocking update check (fire-and-forget)
117
+ checkForUpdates();
118
+
119
+ // Default no-args: launch chat (or onboarding if first run)
120
+ if (args.length === 0) {
121
+ const { needsOnboarding } = await import('./commands/onboarding');
122
+ if (needsOnboarding()) {
123
+ args[0] = 'onboarding';
124
+ } else {
125
+ args[0] = 'chat';
126
+ }
127
+ }
128
+
129
+ // Initialize the application (SQLite, LLM router, etc.)
130
+ const { initApp, shutdownApp } = await import('./app');
131
+
132
+ // Register shutdown hooks for clean exit.
133
+ // Use process.exitCode instead of process.exit() to avoid racing
134
+ // with finally blocks and async cleanup in the TUI.
135
+ let cleaningUp = false;
136
+ const cleanup = async () => {
137
+ if (cleaningUp) {
138
+ return;
139
+ } // Prevent double-cleanup on rapid Ctrl+C
140
+ cleaningUp = true;
141
+ await shutdownApp();
142
+ process.exitCode = 0;
143
+ };
144
+
145
+ process.on('SIGINT', cleanup);
146
+ process.on('SIGTERM', cleanup);
147
+
148
+ try {
149
+ // Commands that don't need full initialization
150
+ const noInitCommands = new Set([
151
+ 'help',
152
+ 'version',
153
+ 'doctor',
154
+ 'onboarding',
155
+ 'upgrade',
156
+ 'update',
157
+ ]);
158
+
159
+ if (!noInitCommands.has(args[0])) {
160
+ // Show brief startup indicator for interactive commands
161
+ if (args[0] === 'chat' && process.stderr.isTTY) {
162
+ process.stderr.write('\x1b[2mStarting Nimbus...\x1b[0m');
163
+ }
164
+ await initApp();
165
+ if (args[0] === 'chat' && process.stderr.isTTY) {
166
+ process.stderr.write('\r\x1b[K');
167
+ }
168
+ }
169
+
170
+ // Import and run CLI command router
171
+ const { runCommand } = await import('./cli');
172
+ await runCommand(args);
173
+
174
+ // After onboarding, clear auth cache, initialize app, and launch chat
175
+ if (args[0] === 'onboarding') {
176
+ // Clear the auth-bridge cache so the router picks up freshly-saved credentials
177
+ try {
178
+ const { clearAuthCache } = await import('./llm/auth-bridge');
179
+ clearAuthCache();
180
+ } catch {
181
+ /* non-critical */
182
+ }
183
+ await initApp();
184
+ await runCommand(['chat']);
185
+ }
186
+ } catch (error: any) {
187
+ const msg = error.message || String(error);
188
+ if (msg.includes('bun:sqlite') || msg.includes('bun:')) {
189
+ console.error(
190
+ 'Error: Nimbus requires the Bun runtime (for bun:sqlite and other built-in APIs).'
191
+ );
192
+ console.error('');
193
+ console.error('If you have Bun installed, run:');
194
+ console.error(' bun src/nimbus.ts');
195
+ console.error('');
196
+ console.error('To install Bun:');
197
+ console.error(' curl -fsSL https://bun.sh/install | bash');
198
+ console.error('');
199
+ console.error('Or install the pre-built binary (no Bun required):');
200
+ console.error(' brew install the-ai-project-co/tap/nimbus');
201
+ console.error(' # or download from GitHub Releases');
202
+ } else if (error.code === 'MODULE_NOT_FOUND') {
203
+ console.error(`Error: Missing module — ${msg}`);
204
+ console.error('Run "bun install" to install dependencies.');
205
+ } else {
206
+ console.error(`Error: ${msg}`);
207
+ }
208
+ process.exit(1);
209
+ } finally {
210
+ await shutdownApp();
211
+ }
212
+ }
213
+
214
+ main();
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Nimbus Plugin System — Public API
3
+ *
4
+ * Re-exports the primary class and types needed by the rest of the
5
+ * application and by external consumers that want to integrate with the
6
+ * plugin system without importing internal submodules directly.
7
+ *
8
+ * @module plugins
9
+ */
10
+
11
+ // Manager (class + singleton)
12
+ export { PluginManager, pluginManager } from './manager';
13
+ export type { PluginInfo, PluginInitResult } from './manager';
14
+
15
+ // Loader (class — exposed for testing and advanced use)
16
+ export { PluginLoader } from './loader';
17
+
18
+ // Public types
19
+ export type {
20
+ NimbusPlugin,
21
+ PluginToolDefinition,
22
+ PluginProviderDefinition,
23
+ PluginManifest,
24
+ PluginPermissionTier,
25
+ LoadedPlugin,
26
+ PluginLoadError,
27
+ } from './types';
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Nimbus Plugin System — Loader
3
+ *
4
+ * Responsible for discovering plugin directories under `~/.nimbus/plugins/`,
5
+ * reading each `nimbus-plugin.json` manifest, dynamically importing the
6
+ * plugin's entry point, and validating that it exports a {@link NimbusPlugin}.
7
+ *
8
+ * Design principles:
9
+ * - A single bad plugin must never crash the host process. All errors are
10
+ * caught, surfaced via the {@link PluginLoadError} array returned from
11
+ * {@link PluginLoader.loadAll}, and emitted as warnings to stderr.
12
+ * - No external dependencies are introduced. Dynamic import (`import()`)
13
+ * is the only mechanism used to load plugin modules.
14
+ * - The loader is stateless between calls; the manager owns the active-plugin
15
+ * map.
16
+ *
17
+ * @module plugins/loader
18
+ */
19
+
20
+ import * as fs from 'node:fs';
21
+ import * as path from 'node:path';
22
+ import { homedir } from 'node:os';
23
+ import type { NimbusPlugin, PluginManifest, LoadedPlugin, PluginLoadError } from './types';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Constants
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /** Name of the manifest file that must exist in every plugin directory. */
30
+ const MANIFEST_FILENAME = 'nimbus-plugin.json';
31
+
32
+ /** Default plugins root directory: ~/.nimbus/plugins/ */
33
+ const DEFAULT_PLUGINS_DIR = path.join(homedir(), '.nimbus', 'plugins');
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Type Guards
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /**
40
+ * Runtime check: does `value` look like a valid {@link PluginManifest}?
41
+ *
42
+ * Only the required fields (`name`, `version`, `main`) are checked.
43
+ */
44
+ function isPluginManifest(value: unknown): value is PluginManifest {
45
+ if (typeof value !== 'object' || value === null) {
46
+ return false;
47
+ }
48
+ const obj = value as Record<string, unknown>;
49
+ return (
50
+ typeof obj['name'] === 'string' &&
51
+ obj['name'].trim().length > 0 &&
52
+ typeof obj['version'] === 'string' &&
53
+ obj['version'].trim().length > 0 &&
54
+ typeof obj['main'] === 'string' &&
55
+ obj['main'].trim().length > 0
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Runtime check: does `value` look like a valid {@link NimbusPlugin}?
61
+ *
62
+ * Only the required fields (`name`, `version`) are checked. Optional
63
+ * arrays and lifecycle hooks are not validated here; if a plugin provides
64
+ * malformed tools the manager will surface errors when registering them.
65
+ */
66
+ function isNimbusPlugin(value: unknown): value is NimbusPlugin {
67
+ if (typeof value !== 'object' || value === null) {
68
+ return false;
69
+ }
70
+ const obj = value as Record<string, unknown>;
71
+ return (
72
+ typeof obj['name'] === 'string' &&
73
+ obj['name'].trim().length > 0 &&
74
+ typeof obj['version'] === 'string' &&
75
+ obj['version'].trim().length > 0
76
+ );
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Loader Class
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /**
84
+ * Discovers, loads, and unloads Nimbus plugins from the filesystem.
85
+ *
86
+ * Intended for use by {@link import('./manager').PluginManager}; most
87
+ * application code should interact with the manager rather than the loader
88
+ * directly.
89
+ */
90
+ export class PluginLoader {
91
+ /** Root directory scanned by {@link discover}. */
92
+ private readonly pluginsDir: string;
93
+
94
+ /** Plugins that have been successfully loaded in this loader instance. */
95
+ private loadedPlugins: LoadedPlugin[] = [];
96
+
97
+ /**
98
+ * @param pluginsDir - Override the default `~/.nimbus/plugins/` scan root.
99
+ * Primarily useful in tests.
100
+ */
101
+ constructor(pluginsDir: string = DEFAULT_PLUGINS_DIR) {
102
+ this.pluginsDir = pluginsDir;
103
+ }
104
+
105
+ // -------------------------------------------------------------------------
106
+ // Public API
107
+ // -------------------------------------------------------------------------
108
+
109
+ /**
110
+ * Scan {@link pluginsDir} for subdirectories that contain a
111
+ * `nimbus-plugin.json` manifest.
112
+ *
113
+ * Entries that are not directories, or whose manifest file is missing, are
114
+ * silently skipped.
115
+ *
116
+ * @returns Absolute paths of discovered plugin directories.
117
+ */
118
+ discover(): string[] {
119
+ if (!fs.existsSync(this.pluginsDir)) {
120
+ return [];
121
+ }
122
+
123
+ let entries: fs.Dirent[];
124
+ try {
125
+ entries = fs.readdirSync(this.pluginsDir, { withFileTypes: true });
126
+ } catch (err: unknown) {
127
+ const msg = err instanceof Error ? err.message : String(err);
128
+ process.stderr.write(
129
+ `[nimbus:plugins] Warning: could not read plugins directory '${this.pluginsDir}': ${msg}\n`
130
+ );
131
+ return [];
132
+ }
133
+
134
+ const discovered: string[] = [];
135
+
136
+ for (const entry of entries) {
137
+ if (!entry.isDirectory()) {
138
+ continue;
139
+ }
140
+
141
+ const pluginDir = path.join(this.pluginsDir, entry.name);
142
+ const manifestPath = path.join(pluginDir, MANIFEST_FILENAME);
143
+
144
+ if (fs.existsSync(manifestPath)) {
145
+ discovered.push(pluginDir);
146
+ }
147
+ }
148
+
149
+ return discovered;
150
+ }
151
+
152
+ /**
153
+ * Load a single plugin from the given directory.
154
+ *
155
+ * Steps:
156
+ * 1. Read and parse `nimbus-plugin.json`.
157
+ * 2. Resolve the `main` path relative to `pluginDir`.
158
+ * 3. Dynamically import the entry point.
159
+ * 4. Extract the default export and validate it as a {@link NimbusPlugin}.
160
+ * 5. Await {@link NimbusPlugin.onLoad} if defined.
161
+ *
162
+ * @param pluginDir - Absolute path to the plugin directory.
163
+ * @returns The loaded plugin record on success.
164
+ * @throws {Error} Descriptive error if any step fails.
165
+ */
166
+ async load(pluginDir: string): Promise<LoadedPlugin> {
167
+ // --- Step 1: Read manifest -----------------------------------------------
168
+ const manifestPath = path.join(pluginDir, MANIFEST_FILENAME);
169
+
170
+ let manifestRaw: string;
171
+ try {
172
+ manifestRaw = fs.readFileSync(manifestPath, 'utf-8');
173
+ } catch (err: unknown) {
174
+ const msg = err instanceof Error ? err.message : String(err);
175
+ throw new Error(`Cannot read manifest at '${manifestPath}': ${msg}`);
176
+ }
177
+
178
+ let manifestData: unknown;
179
+ try {
180
+ manifestData = JSON.parse(manifestRaw);
181
+ } catch (err: unknown) {
182
+ const msg = err instanceof Error ? err.message : String(err);
183
+ throw new Error(`Invalid JSON in manifest '${manifestPath}': ${msg}`);
184
+ }
185
+
186
+ if (!isPluginManifest(manifestData)) {
187
+ throw new Error(
188
+ `Manifest '${manifestPath}' is missing required fields ` +
189
+ `('name', 'version', and 'main' must be non-empty strings).`
190
+ );
191
+ }
192
+
193
+ const manifest = manifestData as PluginManifest;
194
+
195
+ // --- Step 2: Resolve entry-point path ------------------------------------
196
+ const mainPath = path.resolve(pluginDir, manifest.main);
197
+
198
+ if (!fs.existsSync(mainPath)) {
199
+ throw new Error(
200
+ `Plugin '${manifest.name}': entry point '${mainPath}' does not exist ` +
201
+ `(resolved from manifest 'main': '${manifest.main}').`
202
+ );
203
+ }
204
+
205
+ // --- Step 3: Dynamic import ----------------------------------------------
206
+ let moduleExports: unknown;
207
+ try {
208
+ // Use a dynamic string to prevent bundlers from statically analyzing this
209
+ // import and attempting to inline plugin code at build time.
210
+ const importPath = mainPath;
211
+ moduleExports = await import(importPath);
212
+ } catch (err: unknown) {
213
+ const msg = err instanceof Error ? err.message : String(err);
214
+ throw new Error(`Plugin '${manifest.name}': failed to import '${mainPath}': ${msg}`);
215
+ }
216
+
217
+ // --- Step 4: Extract and validate the default export ---------------------
218
+ const rawPlugin =
219
+ moduleExports !== null &&
220
+ typeof moduleExports === 'object' &&
221
+ 'default' in (moduleExports as Record<string, unknown>)
222
+ ? (moduleExports as Record<string, unknown>)['default']
223
+ : moduleExports;
224
+
225
+ if (!isNimbusPlugin(rawPlugin)) {
226
+ throw new Error(
227
+ `Plugin '${manifest.name}': entry point '${mainPath}' must default-export ` +
228
+ `an object with at least 'name' (string) and 'version' (string) fields. ` +
229
+ `Received: ${JSON.stringify(rawPlugin)}`
230
+ );
231
+ }
232
+
233
+ const plugin = rawPlugin as NimbusPlugin;
234
+
235
+ // Sanity check: manifest name should match the exported plugin name.
236
+ // This is a warning only — mismatches are common during development.
237
+ if (plugin.name !== manifest.name) {
238
+ process.stderr.write(
239
+ `[nimbus:plugins] Warning: manifest 'name' ('${manifest.name}') does not ` +
240
+ `match plugin export 'name' ('${plugin.name}') in '${pluginDir}'. ` +
241
+ `Using the exported name.\n`
242
+ );
243
+ }
244
+
245
+ // --- Step 5: Call onLoad lifecycle hook ----------------------------------
246
+ if (typeof plugin.onLoad === 'function') {
247
+ try {
248
+ await plugin.onLoad();
249
+ } catch (err: unknown) {
250
+ const msg = err instanceof Error ? err.message : String(err);
251
+ throw new Error(`Plugin '${plugin.name}' onLoad() rejected: ${msg}`);
252
+ }
253
+ }
254
+
255
+ const record: LoadedPlugin = { plugin, pluginDir };
256
+ this.loadedPlugins.push(record);
257
+ return record;
258
+ }
259
+
260
+ /**
261
+ * Discover and load all plugins found in {@link pluginsDir}.
262
+ *
263
+ * Plugin load failures are collected rather than thrown, so that one bad
264
+ * plugin does not prevent healthy plugins from loading.
265
+ *
266
+ * @returns An object containing:
267
+ * - `loaded`: successfully loaded plugin records.
268
+ * - `errors`: descriptions of plugins that failed to load.
269
+ */
270
+ async loadAll(): Promise<{ loaded: LoadedPlugin[]; errors: PluginLoadError[] }> {
271
+ const discovered = this.discover();
272
+ const loaded: LoadedPlugin[] = [];
273
+ const errors: PluginLoadError[] = [];
274
+
275
+ for (const pluginDir of discovered) {
276
+ try {
277
+ const record = await this.load(pluginDir);
278
+ loaded.push(record);
279
+ } catch (err: unknown) {
280
+ const reason = err instanceof Error ? err.message : String(err);
281
+ process.stderr.write(
282
+ `[nimbus:plugins] Warning: failed to load plugin from '${pluginDir}': ${reason}\n`
283
+ );
284
+ errors.push({ pluginDir, reason });
285
+ }
286
+ }
287
+
288
+ return { loaded, errors };
289
+ }
290
+
291
+ /**
292
+ * Call {@link NimbusPlugin.onUnload} on every plugin that was loaded by
293
+ * this loader instance.
294
+ *
295
+ * Errors from individual `onUnload` callbacks are logged to stderr but
296
+ * do not abort the unload sequence.
297
+ */
298
+ async unloadAll(): Promise<void> {
299
+ for (const { plugin } of this.loadedPlugins) {
300
+ if (typeof plugin.onUnload === 'function') {
301
+ try {
302
+ await plugin.onUnload();
303
+ } catch (err: unknown) {
304
+ const msg = err instanceof Error ? err.message : String(err);
305
+ process.stderr.write(
306
+ `[nimbus:plugins] Warning: plugin '${plugin.name}' onUnload() threw: ${msg}\n`
307
+ );
308
+ }
309
+ }
310
+ }
311
+
312
+ this.loadedPlugins = [];
313
+ }
314
+
315
+ // -------------------------------------------------------------------------
316
+ // Accessors
317
+ // -------------------------------------------------------------------------
318
+
319
+ /**
320
+ * Returns all plugins successfully loaded by this loader instance.
321
+ * The array is a snapshot — mutations do not affect the loader's internal
322
+ * state.
323
+ */
324
+ getLoaded(): LoadedPlugin[] {
325
+ return [...this.loadedPlugins];
326
+ }
327
+
328
+ /**
329
+ * The root directory this loader scans for plugins.
330
+ */
331
+ get directory(): string {
332
+ return this.pluginsDir;
333
+ }
334
+ }