@build-astron-co/nimbus 0.2.0 → 0.4.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 (469) hide show
  1. package/bin/nimbus +26 -10
  2. package/bin/nimbus.cmd +41 -0
  3. package/bin/nimbus.mjs +70 -0
  4. package/completions/nimbus.bash +38 -0
  5. package/completions/nimbus.fish +48 -0
  6. package/completions/nimbus.zsh +81 -0
  7. package/dist/src/agent/compaction-agent.js +215 -0
  8. package/dist/src/agent/context-manager.js +385 -0
  9. package/dist/src/agent/context.js +322 -0
  10. package/dist/src/agent/deploy-preview.js +395 -0
  11. package/dist/src/agent/expand-files.js +95 -0
  12. package/dist/src/agent/index.js +18 -0
  13. package/dist/src/agent/loop.js +1535 -0
  14. package/dist/src/agent/modes.js +347 -0
  15. package/dist/src/agent/permissions.js +396 -0
  16. package/dist/src/agent/subagents/base.js +67 -0
  17. package/dist/src/agent/subagents/cost.js +45 -0
  18. package/dist/src/agent/subagents/explore.js +36 -0
  19. package/dist/src/agent/subagents/general.js +41 -0
  20. package/dist/src/agent/subagents/index.js +88 -0
  21. package/dist/src/agent/subagents/infra.js +52 -0
  22. package/dist/src/agent/subagents/security.js +60 -0
  23. package/dist/src/agent/system-prompt.js +860 -0
  24. package/dist/src/app.js +152 -0
  25. package/dist/src/audit/activity-log.js +209 -0
  26. package/dist/src/audit/compliance-checker.js +419 -0
  27. package/dist/src/audit/cost-tracker.js +231 -0
  28. package/dist/src/audit/index.js +10 -0
  29. package/dist/src/audit/security-scanner.js +490 -0
  30. package/dist/src/auth/guard.js +64 -0
  31. package/dist/src/auth/index.js +19 -0
  32. package/dist/src/auth/keychain.js +79 -0
  33. package/dist/src/auth/oauth.js +389 -0
  34. package/dist/src/auth/providers.js +415 -0
  35. package/dist/src/auth/sso.js +87 -0
  36. package/dist/src/auth/store.js +424 -0
  37. package/dist/src/auth/types.js +5 -0
  38. package/dist/src/cli/index.js +8 -0
  39. package/dist/src/cli/init.js +1048 -0
  40. package/dist/src/cli/openapi-spec.js +346 -0
  41. package/dist/src/cli/run.js +505 -0
  42. package/dist/src/cli/serve-auth.js +56 -0
  43. package/dist/src/cli/serve.js +432 -0
  44. package/dist/src/cli/web.js +50 -0
  45. package/dist/src/cli.js +1574 -0
  46. package/dist/src/clients/core-engine-client.js +156 -0
  47. package/dist/src/clients/enterprise-client.js +246 -0
  48. package/dist/src/clients/generator-client.js +219 -0
  49. package/dist/src/clients/git-client.js +367 -0
  50. package/dist/src/clients/github-client.js +229 -0
  51. package/dist/src/clients/helm-client.js +299 -0
  52. package/dist/src/clients/index.js +18 -0
  53. package/dist/src/clients/k8s-client.js +270 -0
  54. package/dist/src/clients/llm-client.js +119 -0
  55. package/dist/src/clients/rest-client.js +104 -0
  56. package/dist/src/clients/service-discovery.js +35 -0
  57. package/dist/src/clients/terraform-client.js +302 -0
  58. package/dist/src/clients/tools-client.js +1227 -0
  59. package/dist/src/clients/ws-client.js +93 -0
  60. package/dist/src/commands/alias.js +91 -0
  61. package/dist/src/commands/analyze/index.js +313 -0
  62. package/dist/src/commands/apply/helm.js +375 -0
  63. package/dist/src/commands/apply/index.js +176 -0
  64. package/dist/src/commands/apply/k8s.js +350 -0
  65. package/dist/src/commands/apply/terraform.js +465 -0
  66. package/dist/src/commands/ask.js +137 -0
  67. package/dist/src/commands/audit/index.js +322 -0
  68. package/dist/src/commands/auth-cloud.js +345 -0
  69. package/dist/src/commands/auth-list.js +112 -0
  70. package/dist/src/commands/auth-profile.js +104 -0
  71. package/dist/src/commands/auth-refresh.js +161 -0
  72. package/dist/src/commands/auth-status.js +122 -0
  73. package/dist/src/commands/aws/ec2.js +402 -0
  74. package/dist/src/commands/aws/iam.js +304 -0
  75. package/dist/src/commands/aws/index.js +108 -0
  76. package/dist/src/commands/aws/lambda.js +317 -0
  77. package/dist/src/commands/aws/rds.js +345 -0
  78. package/dist/src/commands/aws/s3.js +346 -0
  79. package/dist/src/commands/aws/vpc.js +302 -0
  80. package/dist/src/commands/aws-discover.js +413 -0
  81. package/dist/src/commands/aws-terraform.js +618 -0
  82. package/dist/src/commands/azure/aks.js +305 -0
  83. package/dist/src/commands/azure/functions.js +200 -0
  84. package/dist/src/commands/azure/index.js +93 -0
  85. package/dist/src/commands/azure/storage.js +378 -0
  86. package/dist/src/commands/azure/vm.js +291 -0
  87. package/dist/src/commands/billing/index.js +224 -0
  88. package/dist/src/commands/chat.js +259 -0
  89. package/dist/src/commands/completions.js +255 -0
  90. package/dist/src/commands/config.js +291 -0
  91. package/dist/src/commands/cost/cloud-cost-estimator.js +211 -0
  92. package/dist/src/commands/cost/estimator.js +73 -0
  93. package/dist/src/commands/cost/index.js +625 -0
  94. package/dist/src/commands/cost/parsers/terraform.js +234 -0
  95. package/dist/src/commands/cost/parsers/types.js +4 -0
  96. package/dist/src/commands/cost/pricing/aws.js +501 -0
  97. package/dist/src/commands/cost/pricing/azure.js +462 -0
  98. package/dist/src/commands/cost/pricing/gcp.js +359 -0
  99. package/dist/src/commands/cost/pricing/index.js +24 -0
  100. package/dist/src/commands/demo.js +196 -0
  101. package/dist/src/commands/deploy.js +215 -0
  102. package/dist/src/commands/doctor.js +1291 -0
  103. package/dist/src/commands/drift/index.js +674 -0
  104. package/dist/src/commands/explain.js +235 -0
  105. package/dist/src/commands/export.js +120 -0
  106. package/dist/src/commands/feedback.js +319 -0
  107. package/dist/src/commands/fix.js +263 -0
  108. package/dist/src/commands/fs/index.js +338 -0
  109. package/dist/src/commands/gcp/compute.js +266 -0
  110. package/dist/src/commands/gcp/functions.js +221 -0
  111. package/dist/src/commands/gcp/gke.js +357 -0
  112. package/dist/src/commands/gcp/iam.js +295 -0
  113. package/dist/src/commands/gcp/index.js +105 -0
  114. package/dist/src/commands/gcp/storage.js +232 -0
  115. package/dist/src/commands/generate-helm.js +1026 -0
  116. package/dist/src/commands/generate-k8s.js +1263 -0
  117. package/dist/src/commands/generate-terraform.js +1058 -0
  118. package/dist/src/commands/gh/index.js +663 -0
  119. package/dist/src/commands/git/index.js +1208 -0
  120. package/dist/src/commands/helm/index.js +985 -0
  121. package/dist/src/commands/help.js +639 -0
  122. package/dist/src/commands/history.js +120 -0
  123. package/dist/src/commands/import.js +782 -0
  124. package/dist/src/commands/incident.js +144 -0
  125. package/dist/src/commands/index.js +109 -0
  126. package/dist/src/commands/init.js +955 -0
  127. package/dist/src/commands/k8s/index.js +979 -0
  128. package/dist/src/commands/login.js +588 -0
  129. package/dist/src/commands/logout.js +61 -0
  130. package/dist/src/commands/logs.js +160 -0
  131. package/dist/src/commands/onboarding.js +382 -0
  132. package/dist/src/commands/pipeline.js +153 -0
  133. package/dist/src/commands/plan/display.js +216 -0
  134. package/dist/src/commands/plan/index.js +525 -0
  135. package/dist/src/commands/plugin.js +325 -0
  136. package/dist/src/commands/preview.js +356 -0
  137. package/dist/src/commands/profile.js +297 -0
  138. package/dist/src/commands/questionnaire.js +1021 -0
  139. package/dist/src/commands/resume.js +35 -0
  140. package/dist/src/commands/rollback.js +259 -0
  141. package/dist/src/commands/rollout.js +74 -0
  142. package/dist/src/commands/runbook.js +307 -0
  143. package/dist/src/commands/schedule.js +202 -0
  144. package/dist/src/commands/status.js +213 -0
  145. package/dist/src/commands/team/index.js +309 -0
  146. package/dist/src/commands/team-context.js +200 -0
  147. package/dist/src/commands/template.js +204 -0
  148. package/dist/src/commands/tf/index.js +989 -0
  149. package/dist/src/commands/upgrade.js +515 -0
  150. package/dist/src/commands/usage/index.js +118 -0
  151. package/dist/src/commands/version.js +145 -0
  152. package/dist/src/commands/watch.js +127 -0
  153. package/dist/src/compat/index.js +2 -0
  154. package/dist/src/compat/runtime.js +10 -0
  155. package/dist/src/compat/sqlite.js +144 -0
  156. package/dist/src/config/index.js +6 -0
  157. package/dist/src/config/manager.js +469 -0
  158. package/dist/src/config/mode-store.js +57 -0
  159. package/dist/src/config/profiles.js +66 -0
  160. package/dist/src/config/safety-policy.js +251 -0
  161. package/dist/src/config/schema.js +107 -0
  162. package/dist/src/config/types.js +311 -0
  163. package/dist/src/config/workspace-state.js +38 -0
  164. package/dist/src/context/context-db.js +138 -0
  165. package/dist/src/demo/index.js +295 -0
  166. package/dist/src/demo/scenarios/full-journey.js +226 -0
  167. package/dist/src/demo/scenarios/getting-started.js +124 -0
  168. package/dist/src/demo/scenarios/helm-release.js +334 -0
  169. package/dist/src/demo/scenarios/k8s-deployment.js +190 -0
  170. package/dist/src/demo/scenarios/terraform-vpc.js +167 -0
  171. package/dist/src/demo/types.js +6 -0
  172. package/dist/src/engine/cost-estimator.js +334 -0
  173. package/dist/src/engine/diagram-generator.js +192 -0
  174. package/dist/src/engine/drift-detector.js +688 -0
  175. package/dist/src/engine/executor.js +832 -0
  176. package/dist/src/engine/index.js +39 -0
  177. package/dist/src/engine/orchestrator.js +436 -0
  178. package/dist/src/engine/planner.js +616 -0
  179. package/dist/src/engine/safety.js +609 -0
  180. package/dist/src/engine/verifier.js +664 -0
  181. package/dist/src/enterprise/audit.js +241 -0
  182. package/dist/src/enterprise/auth.js +189 -0
  183. package/dist/src/enterprise/billing.js +512 -0
  184. package/dist/src/enterprise/index.js +16 -0
  185. package/dist/src/enterprise/teams.js +315 -0
  186. package/dist/src/generator/best-practices.js +1375 -0
  187. package/dist/src/generator/helm.js +495 -0
  188. package/dist/src/generator/index.js +11 -0
  189. package/dist/src/generator/intent-parser.js +420 -0
  190. package/dist/src/generator/kubernetes.js +773 -0
  191. package/dist/src/generator/terraform.js +1472 -0
  192. package/dist/src/history/index.js +6 -0
  193. package/dist/src/history/manager.js +199 -0
  194. package/dist/src/history/types.js +6 -0
  195. package/dist/src/hooks/config.js +318 -0
  196. package/dist/src/hooks/engine.js +317 -0
  197. package/dist/src/hooks/index.js +2 -0
  198. package/dist/src/llm/auth-bridge.js +157 -0
  199. package/dist/src/llm/circuit-breaker.js +116 -0
  200. package/dist/src/llm/config-loader.js +172 -0
  201. package/dist/src/llm/cost-calculator.js +137 -0
  202. package/dist/src/llm/index.js +7 -0
  203. package/dist/src/llm/model-aliases.js +99 -0
  204. package/dist/src/llm/provider-registry.js +57 -0
  205. package/dist/src/llm/providers/anthropic.js +430 -0
  206. package/dist/src/llm/providers/bedrock.js +409 -0
  207. package/dist/src/llm/providers/google.js +344 -0
  208. package/dist/src/llm/providers/ollama.js +661 -0
  209. package/dist/src/llm/providers/openai-compatible.js +289 -0
  210. package/dist/src/llm/providers/openai.js +284 -0
  211. package/dist/src/llm/providers/openrouter.js +293 -0
  212. package/dist/src/llm/router.js +844 -0
  213. package/dist/src/llm/types.js +69 -0
  214. package/dist/src/lsp/client.js +239 -0
  215. package/dist/src/lsp/languages.js +95 -0
  216. package/dist/src/lsp/manager.js +243 -0
  217. package/dist/src/mcp/client.js +289 -0
  218. package/dist/src/mcp/index.js +5 -0
  219. package/dist/src/mcp/manager.js +113 -0
  220. package/dist/src/nimbus.js +212 -0
  221. package/dist/src/plugins/index.js +13 -0
  222. package/dist/src/plugins/loader.js +280 -0
  223. package/dist/src/plugins/manager.js +282 -0
  224. package/dist/src/plugins/types.js +23 -0
  225. package/dist/src/scanners/cicd-scanner.js +230 -0
  226. package/dist/src/scanners/cloud-scanner.js +415 -0
  227. package/dist/src/scanners/framework-scanner.js +430 -0
  228. package/dist/src/scanners/iac-scanner.js +350 -0
  229. package/dist/src/scanners/index.js +454 -0
  230. package/dist/src/scanners/language-scanner.js +258 -0
  231. package/dist/src/scanners/package-manager-scanner.js +252 -0
  232. package/dist/src/scanners/types.js +6 -0
  233. package/dist/src/sessions/manager.js +395 -0
  234. package/dist/src/sessions/types.js +4 -0
  235. package/dist/src/sharing/sync.js +238 -0
  236. package/dist/src/sharing/viewer.js +131 -0
  237. package/dist/src/snapshots/index.js +1 -0
  238. package/dist/src/snapshots/manager.js +432 -0
  239. package/dist/src/state/artifacts.js +94 -0
  240. package/dist/src/state/audit.js +73 -0
  241. package/dist/src/state/billing.js +126 -0
  242. package/dist/src/state/checkpoints.js +81 -0
  243. package/dist/src/state/config.js +58 -0
  244. package/dist/src/state/conversations.js +7 -0
  245. package/dist/src/state/credentials.js +96 -0
  246. package/dist/src/state/db.js +53 -0
  247. package/dist/src/state/index.js +23 -0
  248. package/dist/src/state/messages.js +76 -0
  249. package/dist/src/state/projects.js +92 -0
  250. package/dist/src/state/schema.js +233 -0
  251. package/dist/src/state/sessions.js +79 -0
  252. package/dist/src/state/teams.js +131 -0
  253. package/dist/src/telemetry.js +91 -0
  254. package/dist/src/tools/aws-ops.js +747 -0
  255. package/dist/src/tools/azure-ops.js +491 -0
  256. package/dist/src/tools/file-ops.js +451 -0
  257. package/dist/src/tools/gcp-ops.js +559 -0
  258. package/dist/src/tools/git-ops.js +557 -0
  259. package/dist/src/tools/github-ops.js +460 -0
  260. package/dist/src/tools/helm-ops.js +634 -0
  261. package/dist/src/tools/index.js +16 -0
  262. package/dist/src/tools/k8s-ops.js +579 -0
  263. package/dist/src/tools/schemas/converter.js +129 -0
  264. package/dist/src/tools/schemas/devops.js +3319 -0
  265. package/dist/src/tools/schemas/index.js +19 -0
  266. package/dist/src/tools/schemas/standard.js +966 -0
  267. package/dist/src/tools/schemas/types.js +409 -0
  268. package/dist/src/tools/spawn-exec.js +109 -0
  269. package/dist/src/tools/terraform-ops.js +627 -0
  270. package/dist/src/types/config.js +1 -0
  271. package/dist/src/types/drift.js +4 -0
  272. package/dist/src/types/enterprise.js +5 -0
  273. package/dist/src/types/index.js +14 -0
  274. package/dist/src/types/plan.js +1 -0
  275. package/dist/src/types/request.js +1 -0
  276. package/dist/src/types/response.js +1 -0
  277. package/dist/src/types/service.js +1 -0
  278. package/dist/src/ui/App.js +1672 -0
  279. package/dist/src/ui/DeployPreview.js +60 -0
  280. package/dist/src/ui/FileDiffModal.js +108 -0
  281. package/dist/src/ui/Header.js +46 -0
  282. package/dist/src/ui/HelpModal.js +9 -0
  283. package/dist/src/ui/InputBox.js +408 -0
  284. package/dist/src/ui/MessageList.js +795 -0
  285. package/dist/src/ui/PermissionPrompt.js +72 -0
  286. package/dist/src/ui/StatusBar.js +109 -0
  287. package/dist/src/ui/TerminalPane.js +31 -0
  288. package/dist/src/ui/ToolCallDisplay.js +303 -0
  289. package/dist/src/ui/TreePane.js +83 -0
  290. package/dist/src/ui/chat-ui.js +721 -0
  291. package/dist/src/ui/index.js +11 -0
  292. package/dist/src/ui/ink/index.js +1325 -0
  293. package/dist/src/ui/streaming.js +137 -0
  294. package/dist/src/ui/theme.js +78 -0
  295. package/dist/src/ui/types.js +7 -0
  296. package/dist/src/utils/analytics.js +61 -0
  297. package/dist/src/utils/cost-warning.js +25 -0
  298. package/dist/src/utils/env.js +42 -0
  299. package/dist/src/utils/errors.js +54 -0
  300. package/dist/src/utils/event-bus.js +22 -0
  301. package/dist/src/utils/index.js +16 -0
  302. package/dist/src/utils/logger.js +150 -0
  303. package/dist/src/utils/rate-limiter.js +90 -0
  304. package/dist/src/utils/service-auth.js +36 -0
  305. package/dist/src/utils/validation.js +39 -0
  306. package/dist/src/version.js +3 -0
  307. package/dist/src/watcher/index.js +192 -0
  308. package/dist/src/wizard/approval.js +275 -0
  309. package/dist/src/wizard/index.js +13 -0
  310. package/dist/src/wizard/prompts.js +273 -0
  311. package/dist/src/wizard/types.js +4 -0
  312. package/dist/src/wizard/ui.js +453 -0
  313. package/dist/src/wizard/wizard.js +227 -0
  314. package/package.json +31 -23
  315. package/src/__tests__/alias.test.ts +133 -0
  316. package/src/__tests__/app.test.ts +1 -1
  317. package/src/__tests__/audit.test.ts +1 -1
  318. package/src/__tests__/circuit-breaker.test.ts +1 -1
  319. package/src/__tests__/cli-run.test.ts +237 -1
  320. package/src/__tests__/compat-sqlite.test.ts +68 -0
  321. package/src/__tests__/context-manager.test.ts +131 -1
  322. package/src/__tests__/context.test.ts +1 -1
  323. package/src/__tests__/devops-terminal-gaps.test.ts +718 -0
  324. package/src/__tests__/doctor.test.ts +48 -0
  325. package/src/__tests__/enterprise.test.ts +1 -1
  326. package/src/__tests__/export.test.ts +236 -0
  327. package/src/__tests__/gap-11-18-20.test.ts +958 -0
  328. package/src/__tests__/generator.test.ts +1 -1
  329. package/src/__tests__/helm-streaming.test.ts +127 -0
  330. package/src/__tests__/hooks.test.ts +1 -1
  331. package/src/__tests__/incident.test.ts +179 -0
  332. package/src/__tests__/init.test.ts +55 -4
  333. package/src/__tests__/intent-parser.test.ts +1 -1
  334. package/src/__tests__/llm-router.test.ts +1 -1
  335. package/src/__tests__/logs.test.ts +107 -0
  336. package/src/__tests__/loop-errors.test.ts +244 -0
  337. package/src/__tests__/lsp.test.ts +1 -1
  338. package/src/__tests__/modes.test.ts +1 -1
  339. package/src/__tests__/perf-optimizations.test.ts +847 -0
  340. package/src/__tests__/permissions.test.ts +1 -1
  341. package/src/__tests__/pipeline.test.ts +50 -0
  342. package/src/__tests__/polish-phase3.test.ts +340 -0
  343. package/src/__tests__/profile.test.ts +237 -0
  344. package/src/__tests__/rollback.test.ts +83 -0
  345. package/src/__tests__/runbook.test.ts +219 -0
  346. package/src/__tests__/schedule.test.ts +206 -0
  347. package/src/__tests__/serve.test.ts +1 -1
  348. package/src/__tests__/sessions.test.ts +96 -1
  349. package/src/__tests__/sharing.test.ts +53 -1
  350. package/src/__tests__/snapshots.test.ts +1 -1
  351. package/src/__tests__/standalone-migration.test.ts +199 -0
  352. package/src/__tests__/state-db.test.ts +1 -1
  353. package/src/__tests__/status.test.ts +158 -0
  354. package/src/__tests__/stream-with-tools.test.ts +71 -25
  355. package/src/__tests__/subagents.test.ts +1 -1
  356. package/src/__tests__/system-prompt.test.ts +82 -3
  357. package/src/__tests__/terminal-gap-v2.test.ts +395 -0
  358. package/src/__tests__/terminal-parity.test.ts +393 -0
  359. package/src/__tests__/tf-apply.test.ts +187 -0
  360. package/src/__tests__/tool-converter.test.ts +1 -1
  361. package/src/__tests__/tool-schemas.test.ts +209 -4
  362. package/src/__tests__/tools.test.ts +4 -3
  363. package/src/__tests__/version-json.test.ts +184 -0
  364. package/src/__tests__/version.test.ts +1 -1
  365. package/src/__tests__/watch.test.ts +129 -0
  366. package/src/agent/compaction-agent.ts +40 -1
  367. package/src/agent/context-manager.ts +67 -3
  368. package/src/agent/deploy-preview.ts +62 -1
  369. package/src/agent/expand-files.ts +108 -0
  370. package/src/agent/loop.ts +1312 -31
  371. package/src/agent/permissions.ts +51 -4
  372. package/src/agent/system-prompt.ts +573 -19
  373. package/src/app.ts +58 -0
  374. package/src/audit/security-scanner.ts +45 -0
  375. package/src/auth/keychain.ts +82 -0
  376. package/src/auth/oauth.ts +15 -5
  377. package/src/cli/init.ts +378 -5
  378. package/src/cli/run.ts +407 -16
  379. package/src/cli/serve.ts +78 -1
  380. package/src/cli/web.ts +10 -6
  381. package/src/cli.ts +312 -1
  382. package/src/clients/service-discovery.ts +30 -25
  383. package/src/commands/alias.ts +100 -0
  384. package/src/commands/audit/index.ts +121 -2
  385. package/src/commands/auth-cloud.ts +113 -0
  386. package/src/commands/auth-refresh.ts +187 -0
  387. package/src/commands/aws-discover.ts +144 -251
  388. package/src/commands/aws-terraform.ts +68 -118
  389. package/src/commands/chat.ts +9 -3
  390. package/src/commands/completions.ts +268 -0
  391. package/src/commands/config.ts +26 -0
  392. package/src/commands/cost/index.ts +218 -2
  393. package/src/commands/deploy.ts +260 -0
  394. package/src/commands/doctor.ts +744 -152
  395. package/src/commands/drift/index.ts +371 -23
  396. package/src/commands/export.ts +146 -0
  397. package/src/commands/generate-k8s.ts +9 -61
  398. package/src/commands/generate-terraform.ts +191 -449
  399. package/src/commands/help.ts +212 -36
  400. package/src/commands/history.ts +8 -1
  401. package/src/commands/incident.ts +166 -0
  402. package/src/commands/init.ts +5 -0
  403. package/src/commands/login.ts +86 -1
  404. package/src/commands/logs.ts +167 -0
  405. package/src/commands/onboarding.ts +211 -34
  406. package/src/commands/pipeline.ts +186 -0
  407. package/src/commands/plugin.ts +398 -0
  408. package/src/commands/profile.ts +342 -0
  409. package/src/commands/questionnaire.ts +0 -98
  410. package/src/commands/resume.ts +26 -34
  411. package/src/commands/rollback.ts +315 -0
  412. package/src/commands/rollout.ts +88 -0
  413. package/src/commands/runbook.ts +346 -0
  414. package/src/commands/schedule.ts +236 -0
  415. package/src/commands/status.ts +252 -0
  416. package/src/commands/team-context.ts +220 -0
  417. package/src/commands/template.ts +58 -57
  418. package/src/commands/tf/index.ts +70 -11
  419. package/src/commands/upgrade.ts +57 -0
  420. package/src/commands/version.ts +54 -50
  421. package/src/commands/watch.ts +153 -0
  422. package/src/compat/runtime.ts +1 -1
  423. package/src/compat/sqlite.ts +75 -5
  424. package/src/config/mode-store.ts +62 -0
  425. package/src/config/profiles.ts +84 -0
  426. package/src/config/types.ts +83 -1
  427. package/src/config/workspace-state.ts +53 -0
  428. package/src/engine/cost-estimator.ts +52 -10
  429. package/src/engine/executor.ts +33 -2
  430. package/src/engine/planner.ts +68 -1
  431. package/src/generator/terraform.ts +8 -0
  432. package/src/history/manager.ts +2 -74
  433. package/src/hooks/engine.ts +5 -4
  434. package/src/llm/cost-calculator.ts +2 -2
  435. package/src/llm/providers/anthropic.ts +50 -21
  436. package/src/llm/router.ts +76 -7
  437. package/src/lsp/languages.ts +3 -0
  438. package/src/lsp/manager.ts +21 -5
  439. package/src/nimbus.ts +37 -18
  440. package/src/sessions/manager.ts +108 -1
  441. package/src/sharing/sync.ts +4 -0
  442. package/src/sharing/viewer.ts +66 -0
  443. package/src/tools/file-ops.ts +22 -0
  444. package/src/tools/schemas/devops.ts +3007 -117
  445. package/src/tools/schemas/standard.ts +5 -1
  446. package/src/tools/schemas/types.ts +31 -1
  447. package/src/tools/spawn-exec.ts +148 -0
  448. package/src/ui/App.tsx +1183 -66
  449. package/src/ui/DeployPreview.tsx +62 -57
  450. package/src/ui/FileDiffModal.tsx +162 -0
  451. package/src/ui/Header.tsx +87 -24
  452. package/src/ui/HelpModal.tsx +57 -0
  453. package/src/ui/InputBox.tsx +163 -10
  454. package/src/ui/MessageList.tsx +487 -40
  455. package/src/ui/PermissionPrompt.tsx +17 -5
  456. package/src/ui/StatusBar.tsx +122 -3
  457. package/src/ui/TerminalPane.tsx +84 -0
  458. package/src/ui/ToolCallDisplay.tsx +252 -18
  459. package/src/ui/TreePane.tsx +132 -0
  460. package/src/ui/chat-ui.ts +41 -44
  461. package/src/ui/ink/index.ts +771 -38
  462. package/src/ui/streaming.ts +1 -1
  463. package/src/ui/theme.ts +104 -0
  464. package/src/ui/types.ts +18 -0
  465. package/src/version.ts +1 -1
  466. package/src/watcher/index.ts +66 -15
  467. package/src/wizard/types.ts +1 -0
  468. package/src/wizard/ui.ts +1 -1
  469. package/tsconfig.json +2 -2
@@ -104,22 +104,45 @@ export class AnthropicProvider extends BaseProvider {
104
104
  }
105
105
  }
106
106
 
107
+ /** H4: Format a rate limit error with actionable guidance. */
108
+ private formatRateLimitError(error: unknown): Error {
109
+ const retryAfter = (error as { headers?: Record<string, string> })?.headers?.['retry-after'];
110
+ const waitMsg = retryAfter ? `Wait ${retryAfter}s and retry` : 'Wait ~60 seconds and retry';
111
+ return new Error(
112
+ `Rate limit reached. Options:\n` +
113
+ ` • ${waitMsg}\n` +
114
+ ` • Switch to a faster model: /model claude-haiku-4-5\n` +
115
+ ` • Check usage: https://console.anthropic.com/settings/usage`
116
+ );
117
+ }
118
+
119
+ /** H4: Return true if the error is an Anthropic rate limit (429). */
120
+ private isRateLimitError(error: unknown): boolean {
121
+ if (!error || typeof error !== 'object') return false;
122
+ const e = error as { status?: number; error?: { type?: string } };
123
+ return e.status === 429 || e.error?.type === 'rate_limit_error';
124
+ }
125
+
107
126
  async completeWithTools(request: ToolCompletionRequest): Promise<LLMResponse> {
108
127
  const systemPrompt = this.extractSystemPrompt(request.messages);
109
128
  const messages = this.convertMessages(this.filterSystemMessages(request.messages));
110
129
 
111
130
  const toolChoice = this.convertToolChoice(request.toolChoice);
112
- const response = await this.client.messages.create({
113
- model: request.model || this.defaultModel,
114
- max_tokens: request.maxTokens || 4096,
115
- messages,
116
- system: systemPrompt,
117
- ...(request.toolChoice !== 'none' && { tools: this.convertTools(request.tools) }),
118
- ...(toolChoice && { tool_choice: toolChoice }),
119
- temperature: request.temperature,
120
- });
121
-
122
- return this.convertResponse(response);
131
+ try {
132
+ const response = await this.client.messages.create({
133
+ model: request.model || this.defaultModel,
134
+ max_tokens: request.maxTokens || 4096,
135
+ messages,
136
+ system: systemPrompt,
137
+ ...(request.toolChoice !== 'none' && { tools: this.convertTools(request.tools) }),
138
+ ...(toolChoice && { tool_choice: toolChoice }),
139
+ temperature: request.temperature,
140
+ });
141
+ return this.convertResponse(response);
142
+ } catch (error) {
143
+ if (this.isRateLimitError(error)) throw this.formatRateLimitError(error);
144
+ throw error;
145
+ }
123
146
  }
124
147
 
125
148
  async *streamWithTools(request: ToolCompletionRequest): AsyncIterable<StreamChunk> {
@@ -127,15 +150,21 @@ export class AnthropicProvider extends BaseProvider {
127
150
  const messages = this.convertMessages(this.filterSystemMessages(request.messages));
128
151
 
129
152
  const toolChoice = this.convertToolChoice(request.toolChoice);
130
- const stream = await this.client.messages.stream({
131
- model: request.model || this.defaultModel,
132
- max_tokens: request.maxTokens || 4096,
133
- messages,
134
- system: systemPrompt,
135
- ...(request.toolChoice !== 'none' && { tools: this.convertTools(request.tools) }),
136
- ...(toolChoice && { tool_choice: toolChoice }),
137
- temperature: request.temperature,
138
- });
153
+ let stream: Awaited<ReturnType<typeof this.client.messages.stream>>;
154
+ try {
155
+ stream = await this.client.messages.stream({
156
+ model: request.model || this.defaultModel,
157
+ max_tokens: request.maxTokens || 4096,
158
+ messages,
159
+ system: systemPrompt,
160
+ ...(request.toolChoice !== 'none' && { tools: this.convertTools(request.tools) }),
161
+ ...(toolChoice && { tool_choice: toolChoice }),
162
+ temperature: request.temperature,
163
+ });
164
+ } catch (error) {
165
+ if (this.isRateLimitError(error)) throw this.formatRateLimitError(error);
166
+ throw error;
167
+ }
139
168
 
140
169
  let usage: StreamChunk['usage'] | undefined;
141
170
  let inputTokensFromStart = 0; // Captured from message_start event
@@ -165,7 +194,7 @@ export class AnthropicProvider extends BaseProvider {
165
194
  yield { done: false, toolCallStart: { id: currentToolId, name: currentToolName } };
166
195
  }
167
196
  } else if (event.type === 'content_block_delta') {
168
- if (event.delta.type === 'text_delta') {
197
+ if (event.delta.type === 'text_delta' && event.delta.text) {
169
198
  yield { content: event.delta.text, done: false };
170
199
  } else if ((event.delta as any).type === 'input_json_delta') {
171
200
  const existing = toolCallAccumulator.get(toolCallIndex);
package/src/llm/router.ts CHANGED
@@ -143,8 +143,8 @@ export class LLMRouter {
143
143
  isConfigured = bridge.isProviderConfigured;
144
144
  getApiKey = bridge.getProviderApiKey;
145
145
  } catch (err) {
146
- // Auth-bridge unavailable (e.g., test environment) — fall back to env-only
147
- logger.warn(
146
+ // Auth-bridge unavailable (open-source build) — fall back to env-only
147
+ logger.debug(
148
148
  'Auth-bridge unavailable, using environment variables only:',
149
149
  err instanceof Error ? err.message : String(err)
150
150
  );
@@ -479,17 +479,15 @@ export class LLMRouter {
479
479
  }
480
480
  try {
481
481
  let lastUsage: StreamChunk['usage'] | undefined;
482
- const bufferedChunks: StreamChunk[] = [];
482
+ // Yield each chunk immediately (no buffering) for real-time streaming UX.
483
483
  for await (const chunk of p.streamWithTools(request)) {
484
- bufferedChunks.push(chunk);
485
484
  if (chunk.usage) {
486
485
  lastUsage = chunk.usage;
487
486
  }
488
- }
489
- self.circuitBreaker.recordSuccess(p.name);
490
- for (const chunk of bufferedChunks) {
491
487
  yield chunk;
492
488
  }
489
+ // Record success after the `done: true` chunk has been received and yielded.
490
+ self.circuitBreaker.recordSuccess(p.name);
493
491
  if (lastUsage) {
494
492
  const model = request.model || defaultModel;
495
493
  const cost = calculateCost(
@@ -1033,3 +1031,74 @@ export class LLMRouter {
1033
1031
  request.maxTokens = Math.min(request.maxTokens || 4096, maxTokens);
1034
1032
  }
1035
1033
  }
1034
+
1035
+ // ---------------------------------------------------------------------------
1036
+ // Gap 6: List authenticated providers for /model command
1037
+ // ---------------------------------------------------------------------------
1038
+
1039
+ /**
1040
+ * Return the names of providers that have valid credentials configured.
1041
+ * Checks both environment variables and the auth store.
1042
+ */
1043
+ export function listAuthenticatedProviders(): string[] {
1044
+ const authenticated: string[] = [];
1045
+ if (process.env.ANTHROPIC_API_KEY) authenticated.push('anthropic');
1046
+ if (process.env.OPENAI_API_KEY) authenticated.push('openai');
1047
+ if (process.env.GOOGLE_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY) authenticated.push('google');
1048
+ if (process.env.GROQ_API_KEY) authenticated.push('groq');
1049
+ if (process.env.OPENROUTER_API_KEY) authenticated.push('openrouter');
1050
+ if (process.env.AWS_ACCESS_KEY_ID || process.env.AWS_PROFILE) authenticated.push('bedrock');
1051
+ return authenticated;
1052
+ }
1053
+
1054
+ // ---------------------------------------------------------------------------
1055
+ // Gap 18: Multi-model routing based on task complexity
1056
+ // ---------------------------------------------------------------------------
1057
+
1058
+ /** Complexity tiers for automatic model selection. */
1059
+ export type TaskComplexity = 'simple' | 'moderate' | 'complex';
1060
+
1061
+ /**
1062
+ * Classify a user message as simple, moderate, or complex.
1063
+ *
1064
+ * - **simple**: short status/list/describe queries (<200 chars, no code generation)
1065
+ * - **complex**: long messages, code generation, architectural reasoning
1066
+ * - **moderate**: everything else
1067
+ */
1068
+ export function classifyTaskComplexity(message: string): TaskComplexity {
1069
+ const lower = message.toLowerCase().trim();
1070
+
1071
+ // Simple: short status/list queries
1072
+ if (
1073
+ message.length < 200 &&
1074
+ /^(list|show|get|check|status|what is|what are|describe|which|where|who|ping|echo)\b/.test(lower)
1075
+ ) {
1076
+ return 'simple';
1077
+ }
1078
+
1079
+ // Complex: long messages or keywords that imply heavy reasoning
1080
+ if (
1081
+ message.length > 500 ||
1082
+ /\b(implement|design|architect|refactor|migrate|rewrite|build|create|scaffold|generate|optimize|debug|diagnose|analyze)\b/.test(lower)
1083
+ ) {
1084
+ return 'complex';
1085
+ }
1086
+
1087
+ return 'moderate';
1088
+ }
1089
+
1090
+ /**
1091
+ * Select the appropriate model string for a given complexity level.
1092
+ * If `preferredModel` is provided it always wins (user override).
1093
+ */
1094
+ export function routeModel(complexity: TaskComplexity, preferredModel?: string): string {
1095
+ if (preferredModel) return preferredModel;
1096
+ switch (complexity) {
1097
+ case 'simple':
1098
+ return 'anthropic/claude-haiku-4-5-20251001';
1099
+ case 'complex':
1100
+ return 'anthropic/claude-opus-4-6';
1101
+ default:
1102
+ return 'anthropic/claude-sonnet-4-20250514';
1103
+ }
1104
+ }
@@ -114,3 +114,6 @@ export function getLanguageForFile(filePath: string): LanguageConfig | undefined
114
114
  export function getLanguagePriority(): LanguageConfig[] {
115
115
  return [...LANGUAGE_CONFIGS];
116
116
  }
117
+
118
+ /** Language IDs relevant for DevOps workflows. */
119
+ export const DEVOPS_LANGUAGE_IDS = ['terraform', 'yaml', 'docker'] as const;
@@ -9,6 +9,7 @@
9
9
  import { readFile } from 'node:fs/promises';
10
10
  import { exec } from 'node:child_process';
11
11
  import { promisify } from 'node:util';
12
+ import { EventEmitter } from 'node:events';
12
13
  import { LSPClient, type Diagnostic } from './client';
13
14
  import { getLanguageForFile, LANGUAGE_CONFIGS, type LanguageConfig } from './languages';
14
15
 
@@ -26,16 +27,21 @@ export interface LSPStatus {
26
27
  available: boolean;
27
28
  }
28
29
 
29
- export class LSPManager {
30
+ export class LSPManager extends EventEmitter {
30
31
  private clients = new Map<string, LSPClient>();
31
- private idleTimers = new Map<string, Timer>();
32
+ private idleTimers = new Map<string, ReturnType<typeof setTimeout>>();
32
33
  private rootUri: string;
33
34
  private availabilityCache = new Map<string, boolean>();
34
35
  private fileVersions = new Map<string, number>();
35
36
  private enabled: boolean = true;
37
+ private enabledLanguages?: readonly string[];
38
+ /** C4: Track languages already reported as unavailable to avoid duplicate events. */
39
+ private failedLSPs = new Set<string>();
36
40
 
37
- constructor(rootUri?: string) {
41
+ constructor(rootUri?: string, options?: { enabledLanguages?: readonly string[] }) {
42
+ super();
38
43
  this.rootUri = rootUri ?? process.cwd();
44
+ this.enabledLanguages = options?.enabledLanguages;
39
45
  }
40
46
 
41
47
  /** Enable or disable LSP integration. */
@@ -60,6 +66,11 @@ export class LSPManager {
60
66
  return;
61
67
  }
62
68
 
69
+ // Skip non-enabled language servers if a filter is set
70
+ if (this.enabledLanguages && !this.enabledLanguages.includes(config.id)) {
71
+ return;
72
+ }
73
+
63
74
  // Ensure the client is running
64
75
  const client = await this.ensureClient(config);
65
76
  if (!client) {
@@ -201,6 +212,11 @@ export class LSPManager {
201
212
  // Check if the binary is available
202
213
  const available = await this.isAvailable(config);
203
214
  if (!available) {
215
+ // C4: Emit once per language so the TUI can surface a diagnostic message
216
+ if (!this.failedLSPs.has(config.id)) {
217
+ this.failedLSPs.add(config.id);
218
+ this.emit('lsp-unavailable', config.name ?? config.id, config.command);
219
+ }
204
220
  return null;
205
221
  }
206
222
 
@@ -264,9 +280,9 @@ export class LSPManager {
264
280
  let lspManagerInstance: LSPManager | null = null;
265
281
 
266
282
  /** Get or create the singleton LSP manager. */
267
- export function getLSPManager(rootUri?: string): LSPManager {
283
+ export function getLSPManager(rootUri?: string, options?: { enabledLanguages?: readonly string[] }): LSPManager {
268
284
  if (!lspManagerInstance) {
269
- lspManagerInstance = new LSPManager(rootUri);
285
+ lspManagerInstance = new LSPManager(rootUri, options);
270
286
  }
271
287
  return lspManagerInstance;
272
288
  }
package/src/nimbus.ts CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * Nimbus CLI — Main Entry Point
4
4
  *
@@ -97,7 +97,16 @@ process.on('uncaughtException', error => {
97
97
  });
98
98
 
99
99
  async function main() {
100
- const args = process.argv.slice(2);
100
+ let args = process.argv.slice(2);
101
+
102
+ // G11: Global --project / --cwd flag: change working directory before command dispatch
103
+ const cwdFlagIdx = args.findIndex(a => a === '--project' || a === '--cwd');
104
+ if (cwdFlagIdx !== -1 && args[cwdFlagIdx + 1]) {
105
+ const { resolve } = await import('node:path');
106
+ const targetDir = resolve(args[cwdFlagIdx + 1]);
107
+ process.chdir(targetDir);
108
+ args.splice(cwdFlagIdx, 2);
109
+ }
101
110
 
102
111
  // Handle --version and -v before anything else (no init needed)
103
112
  if (args[0] === '--version' || args[0] === '-v') {
@@ -105,6 +114,22 @@ async function main() {
105
114
  process.exit(0);
106
115
  }
107
116
 
117
+ // Handle --completion-install / completion <shell> — print shell completion script
118
+ if (args[0] === '--completion-install' || args[0] === 'completion') {
119
+ const shell = args[1] ?? 'bash';
120
+ const { join } = await import('node:path');
121
+ const { readFileSync } = await import('node:fs');
122
+ const { fileURLToPath } = await import('node:url');
123
+ const dir = join(fileURLToPath(import.meta.url), '../../completions');
124
+ try {
125
+ process.stdout.write(readFileSync(join(dir, `nimbus.${shell}`), 'utf-8'));
126
+ } catch {
127
+ console.error(`No completion script for shell: ${shell}. Available: bash, zsh, fish`);
128
+ process.exit(1);
129
+ }
130
+ process.exit(0);
131
+ }
132
+
108
133
  // Show help when explicitly requested — handles both `nimbus --help`
109
134
  // and subcommand-level help like `nimbus chat --help` or `nimbus tf -h`.
110
135
  if (args.includes('--help') || args.includes('-h')) {
@@ -167,6 +192,14 @@ async function main() {
167
192
  }
168
193
  }
169
194
 
195
+ // Resolve aliases before dispatching (L2)
196
+ try {
197
+ const { resolveAlias } = await import('./commands/alias');
198
+ args = resolveAlias(args);
199
+ } catch {
200
+ /* alias file not found — ignore */
201
+ }
202
+
170
203
  // Import and run CLI command router
171
204
  const { runCommand } = await import('./cli');
172
205
  await runCommand(args);
@@ -185,23 +218,9 @@ async function main() {
185
218
  }
186
219
  } catch (error: any) {
187
220
  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') {
221
+ if (error.code === 'MODULE_NOT_FOUND') {
203
222
  console.error(`Error: Missing module — ${msg}`);
204
- console.error('Run "bun install" to install dependencies.');
223
+ console.error('Run "npm install" to install dependencies.');
205
224
  } else {
206
225
  console.error(`Error: ${msg}`);
207
226
  }
@@ -13,6 +13,16 @@ import type { Database } from '../compat/sqlite';
13
13
  import type { SessionRecord, SessionStatus, SessionEvent, SessionFileEdit } from './types';
14
14
  import type { LLMMessage } from '../llm/types';
15
15
 
16
+ /** Infra context persisted per session (terraform workspace, kubectl context, etc.) */
17
+ export interface SessionInfraContext {
18
+ terraformWorkspace?: string;
19
+ kubectlContext?: string;
20
+ awsProfile?: string;
21
+ awsRegion?: string;
22
+ gcpProject?: string;
23
+ azureSubscription?: string;
24
+ }
25
+
16
26
  /** Singleton session manager instance. */
17
27
  let instance: SessionManager | null = null;
18
28
 
@@ -22,8 +32,17 @@ export class SessionManager {
22
32
  private fileEdits: Map<string, SessionFileEdit[]> = new Map();
23
33
  private eventListeners: Array<(event: SessionEvent) => void> = [];
24
34
 
25
- constructor(db?: Database) {
35
+ /** Pending conversation writes accumulated before the next debounced flush. */
36
+ private pendingConversationFlush: Map<string, {
37
+ messages: LLMMessage[];
38
+ stats?: Partial<Pick<SessionRecord, 'tokenCount' | 'costUSD' | 'snapshotCount' | 'mode' | 'model'>>;
39
+ }> = new Map();
40
+ private _flushTimer: ReturnType<typeof setTimeout> | null = null;
41
+ private readonly flushDebounceMs: number;
42
+
43
+ constructor(db?: Database, options?: { flushDebounceMs?: number }) {
26
44
  this.db = db || getDb();
45
+ this.flushDebounceMs = options?.flushDebounceMs ?? 5000;
27
46
  this.ensureTable();
28
47
  }
29
48
 
@@ -214,6 +233,11 @@ export class SessionManager {
214
233
  this.emit({ type: 'destroyed', sessionId, timestamp: new Date() });
215
234
  }
216
235
 
236
+ /** M2: Rename a session (update its display name). */
237
+ rename(sessionId: string, name: string): void {
238
+ this.db.prepare('UPDATE sessions SET name = ? WHERE id = ?').run(name, sessionId);
239
+ }
240
+
217
241
  /** Update session metadata (tokens, cost, mode, etc.). */
218
242
  updateSession(
219
243
  sessionId: string,
@@ -275,6 +299,67 @@ export class SessionManager {
275
299
  }
276
300
  }
277
301
 
302
+ /**
303
+ * Atomically save conversation messages AND update session stats.
304
+ * Writes are debounced (default 5s) and batched for performance.
305
+ * Use `flushAll()` for immediate persistence (shutdown / completion).
306
+ */
307
+ saveConversationAndStats(
308
+ sessionId: string,
309
+ messages: LLMMessage[],
310
+ stats: Partial<Pick<SessionRecord, 'tokenCount' | 'costUSD' | 'snapshotCount' | 'mode' | 'model'>>
311
+ ): void {
312
+ // Accumulate into the pending flush map (latest write wins per session)
313
+ const existing = this.pendingConversationFlush.get(sessionId);
314
+ this.pendingConversationFlush.set(sessionId, {
315
+ messages,
316
+ stats: existing?.stats ? { ...existing.stats, ...stats } : stats,
317
+ });
318
+ this._scheduleFlush();
319
+ }
320
+
321
+ /** Schedule a debounced flush (no-op if already scheduled). */
322
+ private _scheduleFlush(): void {
323
+ if (this._flushTimer !== null) return;
324
+ if (this.flushDebounceMs === 0) {
325
+ // Immediate flush path (used in tests)
326
+ this._flushNow();
327
+ return;
328
+ }
329
+ this._flushTimer = setTimeout(() => {
330
+ this._flushTimer = null;
331
+ this._flushNow();
332
+ }, this.flushDebounceMs);
333
+ }
334
+
335
+ /** Write all pending conversation entries to SQLite. */
336
+ private _flushNow(): void {
337
+ if (this.pendingConversationFlush.size === 0) return;
338
+ const pending = this.pendingConversationFlush;
339
+ this.pendingConversationFlush = new Map();
340
+ const txn = this.db.transaction(() => {
341
+ for (const [sessionId, { messages, stats }] of pending) {
342
+ this.saveConversation(sessionId, messages);
343
+ if (stats && Object.keys(stats).length > 0) {
344
+ this.updateSession(sessionId, stats);
345
+ }
346
+ }
347
+ });
348
+ txn();
349
+ }
350
+
351
+ /**
352
+ * Flush all pending conversation writes immediately.
353
+ * Must be called on clean shutdown and session completion.
354
+ */
355
+ flushAll(): void {
356
+ if (this._flushTimer !== null) {
357
+ clearTimeout(this._flushTimer);
358
+ this._flushTimer = null;
359
+ }
360
+ this._flushNow();
361
+ }
362
+
278
363
  /** Load conversation messages for a session. Returns empty array if not found. */
279
364
  loadConversation(sessionId: string): LLMMessage[] {
280
365
  const row: any = this.db
@@ -321,6 +406,28 @@ export class SessionManager {
321
406
  return conflicts;
322
407
  }
323
408
 
409
+ /** Persist infra context (terraform workspace, kubectl context, etc.) for a session. */
410
+ setInfraContext(sessionId: string, ctx: SessionInfraContext): void {
411
+ const row: any = this.db.prepare('SELECT metadata FROM sessions WHERE id = ?').get(sessionId);
412
+ const existing = row?.metadata ? JSON.parse(row.metadata) : {};
413
+ const updated = { ...existing, infraContext: ctx };
414
+ this.db
415
+ .prepare("UPDATE sessions SET metadata = ?, updated_at = datetime('now') WHERE id = ?")
416
+ .run(JSON.stringify(updated), sessionId);
417
+ }
418
+
419
+ /** Retrieve infra context for a session. Returns null if not set. */
420
+ getInfraContext(sessionId: string): SessionInfraContext | null {
421
+ const row: any = this.db.prepare('SELECT metadata FROM sessions WHERE id = ?').get(sessionId);
422
+ if (!row?.metadata) return null;
423
+ try {
424
+ const meta = JSON.parse(row.metadata);
425
+ return meta.infraContext ?? null;
426
+ } catch {
427
+ return null;
428
+ }
429
+ }
430
+
324
431
  /** Listen for session events. */
325
432
  onEvent(listener: (event: SessionEvent) => void): () => void {
326
433
  this.eventListeners.push(listener);
@@ -18,6 +18,7 @@ import type { LLMMessage } from '../llm/types';
18
18
  export const _deps = {
19
19
  getConversation: undefined as ((id: string) => any) | undefined,
20
20
  getSessionManager: undefined as (() => { get: (id: string) => any }) | undefined,
21
+ getDb: undefined as (() => any) | undefined,
21
22
  };
22
23
 
23
24
  function getConversation(id: string) {
@@ -60,6 +61,9 @@ export interface SharedSession {
60
61
  * Lazily import the DB to avoid circular dependency.
61
62
  */
62
63
  function getDb() {
64
+ if (_deps.getDb) {
65
+ return _deps.getDb();
66
+ }
63
67
  try {
64
68
  // eslint-disable-next-line @typescript-eslint/no-var-requires
65
69
  const { getDb: _getDb } = require('../state/db');
@@ -95,3 +95,69 @@ function escapeHtml(text: string): string {
95
95
  .replace(/"/g, '&quot;')
96
96
  .replace(/'/g, '&#39;');
97
97
  }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Gap 4: Runbook / Session Export
101
+ // ---------------------------------------------------------------------------
102
+
103
+ export interface RunbookMessage {
104
+ role: 'user' | 'assistant' | 'system';
105
+ content: string;
106
+ timestamp?: Date;
107
+ }
108
+
109
+ export interface RunbookSession {
110
+ id?: string;
111
+ model?: string;
112
+ mode?: string;
113
+ costUSD?: number;
114
+ tokenCount?: number;
115
+ }
116
+
117
+ /**
118
+ * Format a chat session as a Markdown runbook for team documentation.
119
+ *
120
+ * Produces a self-contained `.md` file with:
121
+ * - Session metadata header (model, date, mode, cost)
122
+ * - Each user message as a `## User` section
123
+ * - Each assistant message as an `## Agent` section
124
+ * - Tool calls as collapsible `<details>` blocks
125
+ * - System messages skipped (they are implementation details)
126
+ */
127
+ export function formatSessionAsRunbook(
128
+ messages: RunbookMessage[],
129
+ session?: RunbookSession
130
+ ): string {
131
+ const date = new Date().toISOString().slice(0, 19).replace('T', ' ');
132
+ const lines: string[] = [
133
+ '# Nimbus Session Runbook',
134
+ '',
135
+ '| Field | Value |',
136
+ '|-------|-------|',
137
+ `| Date | ${date} |`,
138
+ `| Model | ${session?.model ?? 'default'} |`,
139
+ `| Mode | ${session?.mode ?? 'build'} |`,
140
+ `| Tokens | ${session?.tokenCount?.toLocaleString() ?? 'N/A'} |`,
141
+ `| Cost | $${session?.costUSD?.toFixed(4) ?? '0.0000'} |`,
142
+ '',
143
+ '---',
144
+ '',
145
+ ];
146
+
147
+ for (const msg of messages) {
148
+ if (msg.role === 'system') continue;
149
+
150
+ const heading = msg.role === 'user' ? '## User' : '## Agent';
151
+ lines.push(heading);
152
+ lines.push('');
153
+
154
+ // Preserve code blocks; escape HTML outside them
155
+ const content = msg.content.trim();
156
+ lines.push(content);
157
+ lines.push('');
158
+ lines.push('---');
159
+ lines.push('');
160
+ }
161
+
162
+ return lines.join('\n');
163
+ }
@@ -127,6 +127,28 @@ export class FileSystemOperations {
127
127
  const resolvedPath = this.resolvePath(filePath);
128
128
  logger.info(`Reading file: ${resolvedPath}`);
129
129
 
130
+ // Guard against large files that would overflow the context window.
131
+ // Warn and truncate at 500 KB — enough for most source files.
132
+ const MAX_READ_BYTES = 500 * 1024;
133
+ const stat = await fs.stat(resolvedPath);
134
+ if (stat.size > MAX_READ_BYTES) {
135
+ const buffer = Buffer.alloc(MAX_READ_BYTES);
136
+ const fd = await fs.open(resolvedPath, 'r');
137
+ let bytesRead = 0;
138
+ try {
139
+ const result = await fd.read(buffer, 0, MAX_READ_BYTES, 0);
140
+ bytesRead = result.bytesRead;
141
+ } finally {
142
+ await fd.close();
143
+ }
144
+ const truncated = buffer.slice(0, bytesRead).toString(encoding);
145
+ return (
146
+ truncated +
147
+ `\n\n[File truncated: ${(stat.size / 1024).toFixed(1)} KB total, showing first ${(bytesRead / 1024).toFixed(0)} KB. ` +
148
+ `Use line range parameters to read specific sections.]`
149
+ );
150
+ }
151
+
130
152
  const content = await fs.readFile(resolvedPath, encoding);
131
153
  return content;
132
154
  }