@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,462 @@
1
+ /**
2
+ * nimbus serve -- Headless HTTP API Server
3
+ *
4
+ * Exposes the Nimbus agent loop as a REST + SSE API designed for
5
+ * consumption by the Web UI, IDE extensions, and third-party integrations.
6
+ *
7
+ * Endpoints:
8
+ * POST /api/chat -- Send a message, receive SSE streaming response
9
+ * POST /api/run -- Non-interactive single prompt (JSON response)
10
+ * GET /api/sessions -- List all sessions
11
+ * GET /api/session/:id -- Session details + conversation messages
12
+ * POST /api/session/:id -- Continue an existing session (SSE streaming)
13
+ * GET /api/health -- Health check
14
+ * GET /api/openapi.json -- OpenAPI 3.1 specification
15
+ *
16
+ * Usage:
17
+ * nimbus serve # localhost:4200
18
+ * nimbus serve --port 8080 # custom port
19
+ * nimbus serve --host 0.0.0.0 # bind to all interfaces
20
+ * nimbus serve --auth admin:secret # enable HTTP Basic Auth
21
+ *
22
+ * @module cli/serve
23
+ */
24
+
25
+ import { Elysia } from 'elysia';
26
+ import { cors } from '@elysiajs/cors';
27
+ import { initApp } from '../app';
28
+ import { runAgentLoop, type ToolCallInfo } from '../agent/loop';
29
+ import { defaultToolRegistry, type ToolResult } from '../tools/schemas/types';
30
+ import { standardTools } from '../tools/schemas/standard';
31
+ import { devopsTools } from '../tools/schemas/devops';
32
+ import { SessionManager } from '../sessions/manager';
33
+ import { saveConversation, getConversation } from '../state/conversations';
34
+ import { shareSession, getSharedSession, listShares } from '../sharing/sync';
35
+ import { ContextManager } from '../agent/context-manager';
36
+ import { getOpenAPISpec } from './openapi-spec';
37
+ import { createAuthMiddleware } from './serve-auth';
38
+ import type { LLMMessage } from '../llm/types';
39
+ import type { AgentMode } from '../agent/system-prompt';
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Public Types
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /** Options for the `nimbus serve` command. */
46
+ export interface ServeOptions {
47
+ /** Port to listen on (default: 4200). */
48
+ port?: number;
49
+ /** Hostname to bind to (default: 'localhost'). */
50
+ host?: string;
51
+ /** HTTP Basic Auth credentials in "user:pass" format. */
52
+ auth?: string;
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Helpers
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /**
60
+ * Ensure the default tool registry is populated.
61
+ * Idempotent -- skips tools that are already registered.
62
+ */
63
+ function ensureToolsRegistered(): void {
64
+ if (defaultToolRegistry.size > 0) {
65
+ return;
66
+ }
67
+ for (const tool of [...standardTools, ...devopsTools]) {
68
+ try {
69
+ defaultToolRegistry.register(tool);
70
+ } catch {
71
+ // Already registered -- skip.
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Validate and narrow a mode string to the AgentMode union.
78
+ */
79
+ function parseMode(mode: string | undefined): AgentMode {
80
+ if (mode === 'plan' || mode === 'build' || mode === 'deploy') {
81
+ return mode;
82
+ }
83
+ return 'build';
84
+ }
85
+
86
+ /**
87
+ * Create an SSE-formatted ReadableStream that runs the agent loop and
88
+ * emits events for text, tool calls, completion, and errors.
89
+ *
90
+ * SSE event types:
91
+ * session -- { id, mode }
92
+ * text -- { content }
93
+ * tool_start -- { id, name, input? }
94
+ * tool_end -- { id, name, output?, isError }
95
+ * done -- { turns, usage, cost }
96
+ * error -- { message }
97
+ */
98
+ function createAgentSSEStream(
99
+ userMessage: string,
100
+ history: LLMMessage[],
101
+ sessionId: string,
102
+ mode: AgentMode,
103
+ model: string | undefined,
104
+ router: any,
105
+ contextManager: ContextManager,
106
+ sessionManager: SessionManager
107
+ ): ReadableStream<Uint8Array> {
108
+ return new ReadableStream<Uint8Array>({
109
+ async start(controller) {
110
+ const encoder = new TextEncoder();
111
+
112
+ const send = (event: string, data: unknown): void => {
113
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
114
+ controller.enqueue(encoder.encode(payload));
115
+ };
116
+
117
+ try {
118
+ send('session', { id: sessionId, mode });
119
+
120
+ const result = await runAgentLoop(userMessage, history, {
121
+ router,
122
+ toolRegistry: defaultToolRegistry,
123
+ mode,
124
+ model,
125
+ sessionId,
126
+ onText: (text: string) => {
127
+ send('text', { content: text });
128
+ },
129
+ onToolCallStart: (toolCall: ToolCallInfo) => {
130
+ send('tool_start', {
131
+ id: toolCall.id,
132
+ name: toolCall.name,
133
+ input: toolCall.input,
134
+ });
135
+ },
136
+ onToolCallEnd: (toolCall: ToolCallInfo, toolResult: ToolResult) => {
137
+ send('tool_end', {
138
+ id: toolCall.id,
139
+ name: toolCall.name,
140
+ output:
141
+ typeof toolResult.output === 'string'
142
+ ? toolResult.output.slice(0, 5000)
143
+ : toolResult.output,
144
+ isError: toolResult.isError,
145
+ });
146
+ },
147
+ });
148
+
149
+ // Persist conversation
150
+ saveConversation(sessionId, userMessage.slice(0, 100), result.messages, model);
151
+
152
+ // Update session stats
153
+ sessionManager.updateSession(sessionId, {
154
+ tokenCount: result.usage.totalTokens,
155
+ costUSD: result.totalCost,
156
+ });
157
+
158
+ send('done', {
159
+ turns: result.turns,
160
+ usage: result.usage,
161
+ cost: result.totalCost,
162
+ });
163
+ } catch (error: unknown) {
164
+ const msg = error instanceof Error ? error.message : String(error);
165
+ send('error', { message: msg });
166
+ } finally {
167
+ controller.close();
168
+ }
169
+ },
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Wrap a ReadableStream as an SSE Response with proper headers.
175
+ */
176
+ function sseResponse(stream: ReadableStream<Uint8Array>): Response {
177
+ return new Response(stream, {
178
+ headers: {
179
+ 'Content-Type': 'text/event-stream',
180
+ 'Cache-Control': 'no-cache',
181
+ Connection: 'keep-alive',
182
+ 'Access-Control-Allow-Origin': '*',
183
+ },
184
+ });
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // Main Command
189
+ // ---------------------------------------------------------------------------
190
+
191
+ /**
192
+ * Start the Nimbus headless API server.
193
+ *
194
+ * Initializes the app context (DB + LLM router), registers tools, sets up
195
+ * all API routes via Elysia, and starts listening.
196
+ */
197
+ export async function serveCommand(options: ServeOptions): Promise<void> {
198
+ const port = options.port ?? 4200;
199
+ const host = options.host ?? 'localhost';
200
+
201
+ // ------------------------------------------------------------------
202
+ // Initialize core systems
203
+ // ------------------------------------------------------------------
204
+
205
+ const { router } = await initApp();
206
+ const sessionManager = SessionManager.getInstance();
207
+ const contextManager = new ContextManager();
208
+
209
+ ensureToolsRegistered();
210
+
211
+ // ------------------------------------------------------------------
212
+ // Build Elysia app
213
+ // ------------------------------------------------------------------
214
+
215
+ const app = new Elysia().use(
216
+ cors({
217
+ origin: true,
218
+ methods: ['GET', 'POST', 'OPTIONS'],
219
+ allowedHeaders: ['Content-Type', 'Authorization'],
220
+ })
221
+ );
222
+
223
+ // Optional HTTP Basic Auth
224
+ if (options.auth) {
225
+ const colonIdx = options.auth.indexOf(':');
226
+ if (colonIdx > 0) {
227
+ const user = options.auth.slice(0, colonIdx);
228
+ const pass = options.auth.slice(colonIdx + 1);
229
+ app.onBeforeHandle(createAuthMiddleware({ username: user, password: pass }));
230
+ }
231
+ }
232
+
233
+ // ------------------------------------------------------------------
234
+ // GET /api/health
235
+ // ------------------------------------------------------------------
236
+
237
+ app.get('/api/health', () => ({
238
+ status: 'ok' as const,
239
+ version: '0.2.0',
240
+ uptime: process.uptime(),
241
+ db: true,
242
+ llm: true,
243
+ }));
244
+
245
+ // ------------------------------------------------------------------
246
+ // GET /api/openapi.json
247
+ // ------------------------------------------------------------------
248
+
249
+ app.get('/api/openapi.json', () => getOpenAPISpec());
250
+
251
+ // ------------------------------------------------------------------
252
+ // GET /api/sessions
253
+ // ------------------------------------------------------------------
254
+
255
+ app.get('/api/sessions', () => ({
256
+ sessions: sessionManager.list(),
257
+ }));
258
+
259
+ // ------------------------------------------------------------------
260
+ // GET /api/session/:id
261
+ // ------------------------------------------------------------------
262
+
263
+ app.get('/api/session/:id', ({ params }: { params: { id: string } }) => {
264
+ const session = sessionManager.get(params.id);
265
+ if (!session) {
266
+ return new Response(JSON.stringify({ error: 'Session not found' }), {
267
+ status: 404,
268
+ headers: { 'Content-Type': 'application/json' },
269
+ });
270
+ }
271
+
272
+ const conversation = getConversation(params.id);
273
+ return {
274
+ session,
275
+ messages: conversation?.messages ?? [],
276
+ };
277
+ });
278
+
279
+ // ------------------------------------------------------------------
280
+ // POST /api/chat -- SSE streaming chat
281
+ // ------------------------------------------------------------------
282
+
283
+ app.post('/api/chat', async ctx => {
284
+ const body = ctx.body as { message: string; sessionId?: string; model?: string; mode?: string };
285
+ const mode = parseMode(body.mode);
286
+
287
+ // Get or create session
288
+ let sessionId = body.sessionId;
289
+ let session = sessionId ? sessionManager.get(sessionId) : null;
290
+
291
+ if (!session) {
292
+ session = sessionManager.create({
293
+ name: `API Session ${new Date().toISOString().slice(0, 16)}`,
294
+ mode,
295
+ model: body.model,
296
+ });
297
+ sessionId = session.id;
298
+ }
299
+
300
+ // Load existing conversation history
301
+ const existing = getConversation(sessionId!);
302
+ const history: LLMMessage[] = existing?.messages ?? [];
303
+
304
+ const stream = createAgentSSEStream(
305
+ body.message,
306
+ history,
307
+ sessionId!,
308
+ mode,
309
+ body.model,
310
+ router,
311
+ contextManager,
312
+ sessionManager
313
+ );
314
+
315
+ return sseResponse(stream);
316
+ });
317
+
318
+ // ------------------------------------------------------------------
319
+ // POST /api/run -- Non-interactive single prompt
320
+ // ------------------------------------------------------------------
321
+
322
+ app.post('/api/run', async ctx => {
323
+ const body = ctx.body as { prompt: string; model?: string; mode?: string };
324
+ const mode = parseMode(body.mode);
325
+
326
+ const session = sessionManager.create({
327
+ name: `Run: ${body.prompt.slice(0, 50)}`,
328
+ mode,
329
+ model: body.model,
330
+ });
331
+
332
+ try {
333
+ const result = await runAgentLoop(body.prompt, [], {
334
+ router,
335
+ toolRegistry: defaultToolRegistry,
336
+ mode,
337
+ model: body.model,
338
+ });
339
+
340
+ // Persist conversation and mark session complete
341
+ saveConversation(session.id, body.prompt.slice(0, 100), result.messages, body.model);
342
+ sessionManager.complete(session.id);
343
+
344
+ // Extract final assistant message
345
+ const lastAssistant = [...result.messages].reverse().find(m => m.role === 'assistant');
346
+
347
+ return {
348
+ sessionId: session.id,
349
+ response: lastAssistant?.content ?? '',
350
+ turns: result.turns,
351
+ usage: result.usage,
352
+ cost: result.totalCost,
353
+ };
354
+ } catch (error: unknown) {
355
+ const msg = error instanceof Error ? error.message : String(error);
356
+ sessionManager.complete(session.id);
357
+ return new Response(JSON.stringify({ error: msg }), {
358
+ status: 500,
359
+ headers: { 'Content-Type': 'application/json' },
360
+ });
361
+ }
362
+ });
363
+
364
+ // ------------------------------------------------------------------
365
+ // POST /api/session/:id -- Continue existing session (SSE)
366
+ // ------------------------------------------------------------------
367
+
368
+ app.post('/api/session/:id', async ctx => {
369
+ const params = ctx.params as { id: string };
370
+ const body = ctx.body as { message: string; model?: string };
371
+ const session = sessionManager.get(params.id);
372
+ if (!session) {
373
+ return new Response(JSON.stringify({ error: 'Session not found' }), {
374
+ status: 404,
375
+ headers: { 'Content-Type': 'application/json' },
376
+ });
377
+ }
378
+
379
+ const existing = getConversation(params.id);
380
+ const history: LLMMessage[] = existing?.messages ?? [];
381
+ const mode = parseMode(session.mode);
382
+
383
+ const stream = createAgentSSEStream(
384
+ body.message,
385
+ history,
386
+ params.id,
387
+ mode,
388
+ body.model ?? session.model,
389
+ router,
390
+ contextManager,
391
+ sessionManager
392
+ );
393
+
394
+ return sseResponse(stream);
395
+ });
396
+
397
+ // ------------------------------------------------------------------
398
+ // POST /api/share -- Share a session
399
+ // ------------------------------------------------------------------
400
+
401
+ app.post('/api/share', ctx => {
402
+ const body = ctx.body as { sessionId: string; isLive?: boolean; ttlDays?: number };
403
+ const shared = shareSession(body.sessionId, {
404
+ isLive: body.isLive,
405
+ ttlDays: body.ttlDays,
406
+ });
407
+
408
+ if (!shared) {
409
+ return new Response(JSON.stringify({ error: 'Session not found' }), {
410
+ status: 404,
411
+ headers: { 'Content-Type': 'application/json' },
412
+ });
413
+ }
414
+
415
+ return {
416
+ shareId: shared.id,
417
+ url: `http://${host}:${port}/nimbus/share/${shared.id}`,
418
+ expiresAt: shared.expiresAt,
419
+ isLive: shared.isLive,
420
+ };
421
+ });
422
+
423
+ // ------------------------------------------------------------------
424
+ // GET /api/share/:id -- Get shared session
425
+ // ------------------------------------------------------------------
426
+
427
+ app.get('/api/share/:id', ({ params }: { params: { id: string } }) => {
428
+ const shared = getSharedSession(params.id);
429
+ if (!shared) {
430
+ return new Response(JSON.stringify({ error: 'Shared session not found or expired' }), {
431
+ status: 404,
432
+ headers: { 'Content-Type': 'application/json' },
433
+ });
434
+ }
435
+ return shared;
436
+ });
437
+
438
+ // ------------------------------------------------------------------
439
+ // GET /api/shares -- List all shares
440
+ // ------------------------------------------------------------------
441
+
442
+ app.get('/api/shares', () => ({
443
+ shares: listShares(),
444
+ }));
445
+
446
+ // ------------------------------------------------------------------
447
+ // Start listening
448
+ // ------------------------------------------------------------------
449
+
450
+ app.listen({ port, hostname: host });
451
+
452
+ console.log(`
453
+ Nimbus API Server
454
+ ─────────────────────────────
455
+ Local: http://${host}:${port}
456
+ Health: http://${host}:${port}/api/health
457
+ OpenAPI: http://${host}:${port}/api/openapi.json
458
+ ${options.auth ? ' Auth: HTTP Basic Auth enabled' : ' Auth: None (use --auth user:pass to enable)'}
459
+
460
+ Press Ctrl+C to stop.
461
+ `);
462
+ }
package/src/cli/web.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * nimbus web -- Start the API server and open the Web UI
3
+ *
4
+ * Convenience command that:
5
+ * 1. Starts `nimbus serve` on the configured port
6
+ * 2. Opens the astron-landing Web UI in the default browser
7
+ *
8
+ * Usage:
9
+ * nimbus web # serve on 4200, open http://localhost:6001/nimbus
10
+ * nimbus web --port 8080 # custom serve port
11
+ * nimbus web --ui-url https://app.example.com/nimbus # custom Web UI URL
12
+ */
13
+
14
+ import { spawn } from 'node:child_process';
15
+ import { serveCommand } from './serve';
16
+
17
+ export interface WebOptions {
18
+ /** Port for nimbus serve (default: 4200). */
19
+ port?: number;
20
+ /** Hostname for nimbus serve (default: 'localhost'). */
21
+ host?: string;
22
+ /** HTTP Basic Auth credentials. */
23
+ auth?: string;
24
+ /** URL of the Web UI (default: http://localhost:6001/nimbus). */
25
+ uiUrl?: string;
26
+ }
27
+
28
+ /**
29
+ * Open a URL in the default browser (cross-platform).
30
+ */
31
+ async function openBrowser(url: string): Promise<void> {
32
+ const { platform } = process;
33
+ const cmd =
34
+ platform === 'darwin'
35
+ ? ['open', url]
36
+ : platform === 'win32'
37
+ ? ['cmd', '/c', 'start', url]
38
+ : ['xdg-open', url];
39
+
40
+ const proc = spawn(cmd[0], cmd.slice(1), { stdio: 'ignore', detached: true });
41
+ proc.unref();
42
+ }
43
+
44
+ /**
45
+ * Run the web command: start serve and open browser.
46
+ */
47
+ export async function webCommand(options: WebOptions): Promise<void> {
48
+ const port = options.port ?? 4200;
49
+ const uiUrl = options.uiUrl ?? 'http://localhost:6001/nimbus';
50
+
51
+ console.log(`Starting Nimbus API server on port ${port}...`);
52
+ console.log(`Opening Web UI at ${uiUrl}\n`);
53
+
54
+ // Open browser after a short delay to let the server start
55
+ setTimeout(() => {
56
+ openBrowser(uiUrl).catch(() => {
57
+ console.log(`Could not open browser. Please visit: ${uiUrl}`);
58
+ });
59
+ }, 1500);
60
+
61
+ // Start the server (this blocks)
62
+ await serveCommand({
63
+ port,
64
+ host: options.host,
65
+ auth: options.auth,
66
+ });
67
+ }