@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
@@ -19,8 +19,12 @@ import {
19
19
  type OnModelChangeCallback,
20
20
  type OnModeChangeCallback,
21
21
  type SessionSummary,
22
+ type OnDiffCallback,
23
+ type OnCostCallback,
24
+ type OnInitCallback,
22
25
  } from '../App';
23
- import type { UIMessage, UIToolCall } from '../types';
26
+ import type { FileDiffDecision } from '../FileDiffModal';
27
+ import type { UIMessage, UIToolCall, DeployPreviewData } from '../types';
24
28
  import { getAppContext } from '../../app';
25
29
  import { runAgentLoop, type AgentLoopResult } from '../../agent/loop';
26
30
  import { buildSystemPrompt, type AgentMode } from '../../agent/system-prompt';
@@ -37,8 +41,14 @@ import {
37
41
  type PermissionSessionState,
38
42
  } from '../../agent/permissions';
39
43
  import { FileWatcher } from '../../watcher';
40
- import { existsSync, readFileSync } from 'node:fs';
44
+ import { HookEngine } from '../../hooks/engine';
45
+ import { getLSPManager } from '../../lsp/manager';
46
+ import { DEVOPS_LANGUAGE_IDS } from '../../lsp/languages';
47
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
41
48
  import { join } from 'node:path';
49
+ import { homedir } from 'node:os';
50
+ import type { InfraContext } from '../../cli/init';
51
+ import { setTheme } from '../theme';
42
52
 
43
53
  export interface InkChatOptions {
44
54
  /** LLM model to use. */
@@ -51,6 +61,8 @@ export interface InkChatOptions {
51
61
  mode?: AgentMode;
52
62
  /** Resume a previous session by ID. */
53
63
  resumeSessionId?: string;
64
+ /** Pre-loaded initial prompt (sent as first user message automatically). */
65
+ initialPrompt?: string;
54
66
  }
55
67
 
56
68
  /**
@@ -65,36 +77,153 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
65
77
  throw new Error('App not initialised. Call initApp() before startInkChat().');
66
78
  }
67
79
 
80
+ // Gap 19: collect any startup warnings so they can be shown as system messages
81
+ let _startupWarnings: string[] = [];
82
+ try {
83
+ const { startupWarnings } = await import('../../app');
84
+ _startupWarnings = startupWarnings;
85
+ } catch { /* non-critical */ }
86
+
87
+ // Gap 2: load theme from ~/.nimbus/config.yaml if present
88
+ try {
89
+ const configPath = join(homedir(), '.nimbus', 'config.yaml');
90
+ if (existsSync(configPath)) {
91
+ const configContent = readFileSync(configPath, 'utf-8');
92
+ const themeMatch = configContent.match(/^theme:\s*(\S+)/m);
93
+ if (themeMatch) {
94
+ setTheme(themeMatch[1]);
95
+ }
96
+ }
97
+ } catch { /* non-critical */ }
98
+
68
99
  // Use mutable refs so /model, /mode, and Tab changes propagate to the agent loop
69
100
  let currentMode: AgentMode = options.mode ?? 'build';
70
101
  let currentModel: string | undefined = options.model;
102
+ // Gap 7 & 10: live infra context discovered at startup
103
+ let currentInfraContext: InfraContext | undefined;
104
+
105
+ // C1: Load prior infra state from ~/.nimbus/infra-state.json before discovery
106
+ const infraStatePath = join(homedir(), '.nimbus', 'infra-state.json');
107
+ let priorInfraState: InfraContext | undefined;
108
+ try {
109
+ if (existsSync(infraStatePath)) {
110
+ const raw = readFileSync(infraStatePath, 'utf-8');
111
+ priorInfraState = JSON.parse(raw) as InfraContext;
112
+ }
113
+ } catch { /* non-critical */ }
114
+
115
+ // H6: Load persisted workspace state as baseline (fresh discovery will override below)
116
+ try {
117
+ const { loadWorkspaceState } = await import('../../config/workspace-state');
118
+ const storedWorkspace = loadWorkspaceState(process.cwd());
119
+ if (!currentInfraContext && Object.keys(storedWorkspace).length > 0) {
120
+ currentInfraContext = storedWorkspace as InfraContext;
121
+ }
122
+ } catch { /* non-critical */ }
123
+
71
124
  const contextManager = new ContextManager({ model: currentModel });
72
125
  const snapshotManager = new SnapshotManager({ projectDir: process.cwd() });
126
+ const lspManager = getLSPManager(process.cwd(), { enabledLanguages: DEVOPS_LANGUAGE_IDS });
73
127
 
74
128
  // Concurrent message guard: prevent overlapping agent loop runs
75
129
  let isRunning = false;
76
130
 
77
- // Eagerly load NIMBUS.md for explicit pass-through to the agent loop
131
+ // Context window warning: warn once per session at 70% usage
132
+ let contextWarningShown = false;
133
+
134
+ // Eagerly load NIMBUS.md for explicit pass-through to the agent loop.
135
+ // On the first run (no NIMBUS.md found), auto-run `nimbus init --quiet`
136
+ // to generate one with detected project context.
78
137
  let nimbusInstructions: string | undefined;
79
138
  const nimbusMdPaths = [
80
139
  join(process.cwd(), 'NIMBUS.md'),
81
140
  join(process.cwd(), '.nimbus', 'NIMBUS.md'),
82
141
  ];
83
- for (const p of nimbusMdPaths) {
84
- if (existsSync(p)) {
85
- try {
86
- nimbusInstructions = readFileSync(p, 'utf-8');
87
- break;
88
- } catch {
89
- /* skip */
142
+
143
+ const foundNimbusMd = nimbusMdPaths.find(p => existsSync(p));
144
+ if (foundNimbusMd) {
145
+ try {
146
+ nimbusInstructions = readFileSync(foundNimbusMd, 'utf-8');
147
+ } catch {
148
+ /* skip */
149
+ }
150
+ } else if (!options.resumeSessionId) {
151
+ // Fresh session with no NIMBUS.md — silently auto-generate one
152
+ try {
153
+ const { runInit } = await import('../../cli/init');
154
+ const result = await runInit({ cwd: process.cwd(), quiet: true });
155
+ // Load the freshly generated NIMBUS.md
156
+ if (result.nimbusmdPath && existsSync(result.nimbusmdPath)) {
157
+ nimbusInstructions = readFileSync(result.nimbusmdPath, 'utf-8');
90
158
  }
159
+ } catch {
160
+ /* init failure is non-critical — proceed without project context */
91
161
  }
92
162
  }
93
163
 
164
+ // G4: If NIMBUS.md is still missing after auto-init attempt, show a prominent banner
165
+ const isNewSessionEarly = !options.resumeSessionId;
166
+ const nimbusMdMissing = !nimbusInstructions;
167
+ // initialMessages array will be populated later; we track the banner flag here
168
+ const showNimbusMdBanner = nimbusMdMissing && isNewSessionEarly;
169
+
170
+ // Initialize hook engine with project dir (loads .nimbus/hooks.yaml if present)
171
+ const hookEngine = new HookEngine(process.cwd());
172
+
94
173
  // Start filesystem watcher for external change awareness
95
174
  const watcher = new FileWatcher(process.cwd());
96
175
  watcher.start();
97
176
 
177
+ // NIMBUS.md live reload (M10): watch for changes to NIMBUS.md mid-session
178
+ // M5: Also notify on DevOps file changes (debounced 30s per file)
179
+ const devopsChangeDebounce = new Map<string, ReturnType<typeof setTimeout>>();
180
+
181
+ watcher.on('change', (changedPath: string) => {
182
+ if (changedPath.endsWith('NIMBUS.md')) {
183
+ try {
184
+ nimbusInstructions = readFileSync(changedPath, 'utf-8');
185
+ addMessage({
186
+ id: crypto.randomUUID(),
187
+ role: 'system',
188
+ content: '[md] NIMBUS.md reloaded — new instructions active for next turn.',
189
+ timestamp: new Date(),
190
+ });
191
+ } catch {
192
+ /* ignore read errors */
193
+ }
194
+ }
195
+
196
+ // M5: Notify on DevOps file changes (debounced 30s per file)
197
+ const filePath = typeof changedPath === 'string' ? changedPath : (changedPath as any)?.path ?? '';
198
+ const isDevOps = /\.(tf|yaml|yml)$|Dockerfile|docker-compose/i.test(filePath);
199
+ if (isDevOps) {
200
+ const existing = devopsChangeDebounce.get(filePath);
201
+ if (existing) clearTimeout(existing);
202
+ const timer = setTimeout(() => {
203
+ devopsChangeDebounce.delete(filePath);
204
+ const relPath = filePath.replace(process.cwd() + '/', '');
205
+ const hint = relPath.endsWith('.tf') ? '/plan' : relPath.includes('yaml') ? '/plan' : '/init';
206
+ addMessage({
207
+ id: crypto.randomUUID(),
208
+ role: 'system',
209
+ content: `[~] File changed: ${relPath} — type ${hint} to review drift impact`,
210
+ timestamp: new Date(),
211
+ });
212
+ }, 30000);
213
+ devopsChangeDebounce.set(filePath, timer);
214
+ }
215
+ });
216
+
217
+ // C4: Surface LSP unavailability as system messages so the user knows diagnostics are disabled
218
+ lspManager.on('lsp-unavailable', (lang: string, cmd: string) => {
219
+ addMessage({
220
+ id: crypto.randomUUID(),
221
+ role: 'system',
222
+ content: `[LSP] ${lang} server (${cmd}) not found — diagnostics disabled.`,
223
+ timestamp: new Date(),
224
+ });
225
+ });
226
+
98
227
  // Create or resume a session for conversation persistence
99
228
  let sessionManager: SessionManager | null = null;
100
229
  let sessionId: string | null = null;
@@ -121,12 +250,74 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
121
250
  sessionId = session.id;
122
251
  }
123
252
  } catch (sessionErr) {
124
- // Session persistence is non-critical warn so user knows history won't be saved
125
- process.stderr.write(
126
- `\x1b[33m Warning: Session persistence unavailable (${sessionErr instanceof Error ? sessionErr.message : 'unknown error'}). Chat history will not be saved.\x1b[0m\n`
127
- );
253
+ // C5: Surface SQLite failure prominently in the TUI (not just stderr)
254
+ const errMsg = sessionErr instanceof Error ? sessionErr.message : String(sessionErr);
255
+ const tuiWarning = `Session persistence unavailable: ${errMsg}. Chat history will NOT be saved this session. Fix: npm install better-sqlite3`;
256
+ _startupWarnings.push(tuiWarning);
257
+ process.stderr.write(`\x1b[33m Warning: ${tuiWarning}\x1b[0m\n`);
128
258
  }
129
259
 
260
+ // Gap 7 & 10: discover live infra context at startup (best-effort, non-blocking)
261
+ try {
262
+ const { discoverInfraContext } = await import('../../cli/init');
263
+ currentInfraContext = await discoverInfraContext(process.cwd());
264
+
265
+ // C1: Merge with prior state (fresh discovery wins per-field)
266
+ if (priorInfraState) {
267
+ currentInfraContext = { ...priorInfraState, ...currentInfraContext };
268
+ }
269
+
270
+ // C1: Persist discovered infra state to ~/.nimbus/infra-state.json
271
+ if (currentInfraContext) {
272
+ try {
273
+ mkdirSync(join(homedir(), '.nimbus'), { recursive: true });
274
+ writeFileSync(infraStatePath, JSON.stringify(currentInfraContext, null, 2), 'utf-8');
275
+ } catch { /* non-critical */ }
276
+
277
+ // H6: Also persist workspace state (terraform workspace + kubectl context) per cwd
278
+ try {
279
+ const { mergeWorkspaceState } = await import('../../config/workspace-state');
280
+ mergeWorkspaceState(process.cwd(), currentInfraContext ?? {});
281
+ } catch { /* non-critical */ }
282
+ }
283
+
284
+ if (sessionManager && sessionId && currentInfraContext) {
285
+ try {
286
+ sessionManager.setInfraContext(sessionId, currentInfraContext);
287
+ } catch { /* non-critical */ }
288
+ }
289
+
290
+ // C4: Set terminal window title with infra context
291
+ try {
292
+ const ctxLabel = [
293
+ currentInfraContext?.terraformWorkspace && `tf:${currentInfraContext.terraformWorkspace}`,
294
+ currentInfraContext?.kubectlContext && `k8s:${currentInfraContext.kubectlContext}`,
295
+ ].filter(Boolean).join(' | ') || 'nimbus';
296
+ process.stdout.write(`\x1b]0;nimbus -- ${ctxLabel}\x07`);
297
+ process.on('exit', () => process.stdout.write('\x1b]0;Terminal\x07'));
298
+ } catch { /* non-critical */ }
299
+ } catch { /* non-critical — infra discovery failure must never block startup */ }
300
+
301
+ // C3: Auto-generate NIMBUS.md if infra is detected but no NIMBUS.md exists
302
+ try {
303
+ const nimbusmdPath = join(process.cwd(), 'NIMBUS.md');
304
+ if (currentInfraContext && !existsSync(nimbusmdPath)) {
305
+ const hasTerraform = (currentInfraContext as { terraformWorkspace?: string }).terraformWorkspace !== undefined
306
+ || existsSync(join(process.cwd(), 'main.tf'))
307
+ || existsSync(join(process.cwd(), 'terraform'));
308
+ const hasK8s = (currentInfraContext as { kubectlContext?: string }).kubectlContext !== undefined;
309
+ const hasHelm = ((currentInfraContext as { helmReleases?: string[] }).helmReleases?.length ?? 0) > 0;
310
+
311
+ if (hasTerraform || hasK8s || hasHelm) {
312
+ const { generateNimbusMd, detectProject } = await import('../../cli/init');
313
+ const detection = detectProject(process.cwd());
314
+ const mdContent = generateNimbusMd(detection, process.cwd(), currentInfraContext);
315
+ writeFileSync(nimbusmdPath, mdContent, 'utf-8');
316
+ process.stderr.write('\x1b[32m [nimbus] Auto-generated NIMBUS.md from detected infra\x1b[0m\n');
317
+ }
318
+ }
319
+ } catch { /* non-critical */ }
320
+
130
321
  // Conversation history shared between turns.
131
322
  // When resuming, restore saved conversation from the session.
132
323
  let history: LLMMessage[] = [];
@@ -139,8 +330,33 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
139
330
  } catch {
140
331
  // Restore is non-critical
141
332
  }
333
+
334
+ // Gap 10: On resume, merge stored infra context with freshly discovered live context
335
+ try {
336
+ const storedInfra = sessionManager.getInfraContext(sessionId);
337
+ // Live context (already discovered above) takes precedence for mutable fields
338
+ currentInfraContext = { ...storedInfra, ...currentInfraContext };
339
+ } catch { /* non-critical */ }
142
340
  }
143
341
 
342
+ // G2 / C1: Build resume context summary message when resuming with infra context
343
+ // Also show when prior state was loaded (even on a new session) to confirm context continuity
344
+ const hasResumeContext = currentInfraContext && (
345
+ currentInfraContext.kubectlContext || currentInfraContext.terraformWorkspace || currentInfraContext.awsAccount
346
+ );
347
+ const showResumeBanner = (options.resumeSessionId || !!priorInfraState) && hasResumeContext;
348
+ const resumeContextMessage: UIMessage | null = showResumeBanner ? {
349
+ id: crypto.randomUUID(),
350
+ role: 'system' as const,
351
+ content: [
352
+ options.resumeSessionId ? 'Resuming session -' : 'Resuming with:',
353
+ currentInfraContext!.terraformWorkspace ? `tf:${currentInfraContext!.terraformWorkspace}` : null,
354
+ currentInfraContext!.kubectlContext ? `k8s:${currentInfraContext!.kubectlContext}` : null,
355
+ currentInfraContext!.awsAccount ? `aws:${currentInfraContext!.awsAccount}` : null,
356
+ ].filter(Boolean).join(' | '),
357
+ timestamp: new Date(),
358
+ } : null;
359
+
144
360
  // AbortController for cancellation (Ctrl+C / Escape)
145
361
  let abortController = new AbortController();
146
362
 
@@ -233,12 +449,88 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
233
449
  });
234
450
  }
235
451
 
452
+ /**
453
+ * Determines whether a tool call requires a deploy preview confirmation in deploy mode.
454
+ * Covers terraform/kubectl/helm plus destructive bash cloud CLI commands.
455
+ */
456
+ function requiresDeployPreview(toolName: string, toolInput: Record<string, unknown>): boolean {
457
+ if (['terraform', 'kubectl', 'helm'].includes(toolName)) return true;
458
+ if (toolName === 'docker') {
459
+ const action = String(toolInput.action ?? '');
460
+ return ['build', 'push', 'stop', 'compose-up', 'compose-down', 'rm', 'prune'].includes(action);
461
+ }
462
+ if (toolName === 'cloud_action') {
463
+ const action = String(toolInput.action ?? '');
464
+ return ['create', 'delete', 'stop'].includes(action);
465
+ }
466
+ if (toolName === 'cfn') {
467
+ const action = String(toolInput.action ?? '');
468
+ return ['create', 'update', 'delete', 'deploy'].includes(action);
469
+ }
470
+ if (toolName === 'bash') {
471
+ const cmd = String(toolInput.command ?? '');
472
+ return /\b(aws\s+\S+\s+delete|aws\s+ec2\s+terminate|gcloud\s+\S+\s+delete|az\s+\S+\s+delete|kubectl\s+delete)\b/.test(cmd);
473
+ }
474
+ return false;
475
+ }
476
+
477
+ /**
478
+ * Show the deploy preview modal and wait for user confirmation.
479
+ * Returns true if the user approves, false if they cancel.
480
+ */
481
+ function promptDeployPreview(tool: string, input: Record<string, unknown>): Promise<boolean> {
482
+ return new Promise(resolve => {
483
+ if (!api) {
484
+ resolve(true); // API not ready — allow by default
485
+ return;
486
+ }
487
+
488
+ const action = typeof input.action === 'string' ? input.action : 'apply';
489
+ const changeAction: 'create' | 'modify' | 'destroy' | 'replace' =
490
+ action.includes('destroy') || action.includes('delete') ? 'destroy' : 'modify';
491
+
492
+ const preview: DeployPreviewData = {
493
+ tool,
494
+ changes: [
495
+ {
496
+ action: changeAction,
497
+ resourceType: tool,
498
+ resourceName: typeof input.command === 'string' ? input.command : action,
499
+ details: typeof input.args === 'string' ? input.args : undefined,
500
+ },
501
+ ],
502
+ };
503
+
504
+ api.requestDeployPreview(preview, decision => {
505
+ resolve(decision === 'approve');
506
+ });
507
+ });
508
+ }
509
+
236
510
  /**
237
511
  * Handle a user message: run the agent loop and stream results back
238
512
  * into the TUI.
239
513
  */
240
514
  // Track the timestamp of each turn so watcher can report changes since last turn
241
515
  let lastTurnTimestamp = Date.now();
516
+ // M2: Track user message count for first-message session rename
517
+ let userMessageCount = 0;
518
+
519
+ /**
520
+ * GAP-20: Parse the ## Tool Timeouts section from NIMBUS.md.
521
+ * Each line has the format: tool_name: milliseconds
522
+ * Returns a Record<string, number> for passing to runAgentLoop as toolTimeouts.
523
+ */
524
+ function parseToolTimeouts(nimbusMd: string): Record<string, number> {
525
+ const result: Record<string, number> = {};
526
+ const match = nimbusMd.match(/##\s+Tool Timeouts\s*\n([\s\S]*?)(?=##|$)/);
527
+ if (!match) return result;
528
+ for (const line of match[1].split('\n')) {
529
+ const m = line.match(/^\s*([a-z_]+)\s*:\s*(\d+)\s*$/);
530
+ if (m) result[m[1]] = parseInt(m[2], 10);
531
+ }
532
+ return result;
533
+ }
242
534
 
243
535
  const onMessage = async (text: string) => {
244
536
  // Gap 1: Prevent concurrent agent loop runs (would corrupt history)
@@ -253,6 +545,17 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
253
545
  }
254
546
  isRunning = true;
255
547
  abortController = new AbortController();
548
+ // Track diff request index within this turn for progress display
549
+ let diffRequestIndex = 0;
550
+
551
+ // M2: Auto-rename session from first user message (semantic name)
552
+ userMessageCount++;
553
+ if (userMessageCount === 1 && sessionManager && sessionId) {
554
+ try {
555
+ const semanticName = text.slice(0, 40).replace(/[^a-z0-9]+/gi, '-').toLowerCase().replace(/^-+|-+$/g, '');
556
+ if (semanticName) sessionManager.rename(sessionId, semanticName);
557
+ } catch { /* non-critical */ }
558
+ }
256
559
 
257
560
  // Prepend external file change summary if any files changed since last turn
258
561
  const changeSummary = watcher.getSummary(lastTurnTimestamp);
@@ -268,9 +571,12 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
268
571
  model: currentModel,
269
572
  cwd: process.cwd(),
270
573
  nimbusInstructions,
574
+ infraContext: currentInfraContext,
271
575
  signal: abortController.signal,
272
576
  contextManager,
273
577
  snapshotManager,
578
+ lspManager,
579
+ hookEngine,
274
580
  onText: chunk => {
275
581
  // Stream text incrementally into the TUI
276
582
  if (!streamingMessageId) {
@@ -296,6 +602,7 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
296
602
  ? (info.input as Record<string, unknown>)
297
603
  : {},
298
604
  status: 'running',
605
+ startTime: info.startTime ?? Date.now(),
299
606
  };
300
607
  activeToolCalls.set(info.id, toolCall);
301
608
  setToolCalls([...activeToolCalls.values()]);
@@ -310,6 +617,28 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
310
617
  };
311
618
  }
312
619
  setToolCalls([...activeToolCalls.values()]);
620
+
621
+ // G6: Surface LSP diagnostics as visible TUI system messages
622
+ if (!toolResult.isError && typeof toolResult.output === 'string'
623
+ && toolResult.output.includes('LSP Diagnostics:')) {
624
+ const diagMatch = toolResult.output.match(/LSP Diagnostics:([\s\S]+?)(?:\n\n|$)/);
625
+ if (diagMatch) {
626
+ addMessage({
627
+ id: crypto.randomUUID(),
628
+ role: 'system',
629
+ content: `⚠ LSP: ${diagMatch[1].trim()}`,
630
+ timestamp: new Date(),
631
+ });
632
+ }
633
+ }
634
+ },
635
+ onToolOutputChunk: (toolId: string, chunk: string) => {
636
+ // Gap 1: stream live output into the running tool call's streamingOutput field
637
+ const existing = activeToolCalls.get(toolId);
638
+ if (existing) {
639
+ existing.streamingOutput = (existing.streamingOutput ?? '') + chunk;
640
+ setToolCalls([...activeToolCalls.values()]);
641
+ }
313
642
  },
314
643
  onUsage: (usage, costUSD) => {
315
644
  // Update the TUI in real-time after each LLM turn
@@ -317,6 +646,34 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
317
646
  tokenCount: usage.totalTokens,
318
647
  costUSD,
319
648
  });
649
+
650
+ // Context window warning at 70% (H5)
651
+ // Use 200k as a reasonable default context window size
652
+ const CTX_MAX = 200_000;
653
+ if (!contextWarningShown && usage.totalTokens > 0) {
654
+ const ratio = usage.totalTokens / CTX_MAX;
655
+ if (ratio >= 0.70) {
656
+ contextWarningShown = true;
657
+ addMessage({
658
+ id: crypto.randomUUID(),
659
+ role: 'system',
660
+ content: `⚠ Context window at ${Math.round(ratio * 100)}% — consider /compact [focus] to preserve the most important context before it auto-compacts at 85%.`,
661
+ timestamp: new Date(),
662
+ });
663
+ }
664
+ }
665
+
666
+ // Track per-turn cost delta for /cost command
667
+ const turnCost = costUSD - previousTotalCost;
668
+ if (turnCost > 0) {
669
+ currentTurn++;
670
+ turnCostLog.push({
671
+ turn: currentTurn,
672
+ costUSD: turnCost,
673
+ tokens: usage.totalTokens,
674
+ });
675
+ previousTotalCost = costUSD;
676
+ }
320
677
  },
321
678
  onCompact: compactResult => {
322
679
  addMessage({
@@ -327,6 +684,16 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
327
684
  });
328
685
  },
329
686
  checkPermission: async (tool, input) => {
687
+ const toolInput =
688
+ input && typeof input === 'object' ? (input as Record<string, unknown>) : {};
689
+ // In deploy mode, show a preview confirmation before infra-mutating tools
690
+ if (currentMode === 'deploy' && requiresDeployPreview(tool.name, toolInput)) {
691
+ const approved = await promptDeployPreview(tool.name, toolInput);
692
+ if (!approved) {
693
+ return 'deny';
694
+ }
695
+ }
696
+
330
697
  const decision = checkPermission(tool, input, permissionState);
331
698
  if (decision === 'allow') {
332
699
  return 'allow';
@@ -337,6 +704,17 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
337
704
  // decision === 'ask': prompt the user
338
705
  return promptPermission(tool, input);
339
706
  },
707
+ requestFileDiff: (path: string, toolName: string, diff: string): Promise<FileDiffDecision> =>
708
+ new Promise(resolve => {
709
+ if (!api) {
710
+ resolve('apply');
711
+ return;
712
+ }
713
+ diffRequestIndex++;
714
+ api.requestFileDiff(path, toolName, diff, resolve, diffRequestIndex);
715
+ }),
716
+ // GAP-20: Pass per-tool timeouts parsed from NIMBUS.md
717
+ toolTimeouts: nimbusInstructions ? parseToolTimeouts(nimbusInstructions) : undefined,
340
718
  });
341
719
 
342
720
  // Clear active tool calls now that the turn is complete
@@ -346,10 +724,13 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
346
724
  // Update history with the full conversation from this turn
347
725
  history = result.messages;
348
726
 
349
- // Persist conversation to SQLite
727
+ // Persist conversation + stats to SQLite atomically
350
728
  if (sessionManager && sessionId) {
351
729
  try {
352
- sessionManager.saveConversation(sessionId, history);
730
+ sessionManager.saveConversationAndStats(sessionId, history, {
731
+ tokenCount: result.usage.totalTokens,
732
+ costUSD: result.totalCost,
733
+ });
353
734
  } catch {
354
735
  /* persistence is non-critical */
355
736
  }
@@ -386,17 +767,7 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
386
767
  costUSD: result.totalCost,
387
768
  });
388
769
 
389
- // Persist session stats to SQLite
390
- if (sessionManager && sessionId) {
391
- try {
392
- sessionManager.updateSession(sessionId, {
393
- tokenCount: result.usage.totalTokens,
394
- costUSD: result.totalCost,
395
- });
396
- } catch {
397
- /* non-critical */
398
- }
399
- }
770
+ // (Session stats already persisted atomically above with saveConversationAndStats)
400
771
  } catch (err: unknown) {
401
772
  const msg = err instanceof Error ? err.message : String(err);
402
773
  addMessage({
@@ -558,6 +929,123 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
558
929
  Object.assign(permissionState, createPermissionState());
559
930
  };
560
931
 
932
+ // -------------------------------------------------------------------------
933
+ // A5: Per-turn cost log for /cost command
934
+ // -------------------------------------------------------------------------
935
+ const turnCostLog: Array<{ turn: number; costUSD: number; tokens: number }> = [];
936
+ let previousTotalCost = 0;
937
+ let currentTurn = 0;
938
+
939
+ /**
940
+ * Handle /diff command — show unstaged git diff.
941
+ */
942
+ const onDiff: OnDiffCallback = async (): Promise<string> => {
943
+ const { spawnSync } = await import('node:child_process');
944
+ const stat = spawnSync('git', ['diff', '--stat'], { encoding: 'utf-8', cwd: process.cwd() });
945
+ const full = spawnSync('git', ['diff'], { encoding: 'utf-8', cwd: process.cwd() });
946
+ const statOut = stat.stdout?.trim() ?? '';
947
+ const fullOut = full.stdout?.trim() ?? '';
948
+ if (!statOut && !fullOut) return 'No unstaged changes.';
949
+ return [statOut, fullOut].filter(Boolean).join('\n\n');
950
+ };
951
+
952
+ /**
953
+ * Handle /cost command — show per-turn cost breakdown.
954
+ */
955
+ const onCost: OnCostCallback = (): string => {
956
+ if (turnCostLog.length === 0) return 'No turns yet.';
957
+ const rows = turnCostLog.map(
958
+ t => ` Turn ${t.turn} ${t.tokens.toLocaleString()} tokens $${t.costUSD.toFixed(4)}`
959
+ );
960
+ const total = turnCostLog.reduce((s, t) => s + t.costUSD, 0);
961
+ const totalTok = turnCostLog.reduce((s, t) => s + t.tokens, 0);
962
+ return [
963
+ 'Cost breakdown:',
964
+ ...rows,
965
+ ` ${'─'.repeat(40)}`,
966
+ ` Total ${totalTok.toLocaleString()} tokens $${total.toFixed(4)}`,
967
+ ].join('\n');
968
+ };
969
+
970
+ /**
971
+ * Handle /init command — regenerate NIMBUS.md from inside the TUI.
972
+ */
973
+ const onInit: OnInitCallback = async (): Promise<string> => {
974
+ const { runInit } = await import('../../cli/init');
975
+ const result = await runInit({ cwd: process.cwd(), quiet: false });
976
+ if (result.nimbusmdPath && existsSync(result.nimbusmdPath)) {
977
+ nimbusInstructions = readFileSync(result.nimbusmdPath, 'utf-8');
978
+ return `NIMBUS.md generated at ${result.nimbusmdPath}. Context updated.`;
979
+ }
980
+ return 'Init complete (no NIMBUS.md generated).';
981
+ };
982
+
983
+ /**
984
+ * Handle /export [filename] — serialize conversation to a runbook markdown file. G16
985
+ */
986
+ const onExport: import('../App').OnExportCallback = async (filename?: string): Promise<string> => {
987
+ const { join } = await import('node:path');
988
+ const { writeFileSync } = await import('node:fs');
989
+
990
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
991
+ const targetFile = filename ?? join(process.cwd(), `nimbus-session-${timestamp}.md`);
992
+
993
+ const lines: string[] = [
994
+ `# Nimbus Session Export`,
995
+ `Session: ${sessionId ?? 'unknown'} | Mode: ${currentMode} | Date: ${new Date().toISOString()}`,
996
+ '',
997
+ '## Conversation',
998
+ '',
999
+ ];
1000
+
1001
+ for (const msg of history) {
1002
+ const role = msg.role === 'user' ? '**User**' : '**Agent**';
1003
+ const contentStr = Array.isArray(msg.content)
1004
+ ? msg.content.map((b: unknown) => (typeof b === 'object' && b !== null && 'text' in b ? (b as {text: string}).text : '')).join('')
1005
+ : String(msg.content ?? '');
1006
+ lines.push(`${role}: ${contentStr}`);
1007
+ lines.push('');
1008
+ }
1009
+
1010
+ writeFileSync(targetFile, lines.join('\n'), 'utf-8');
1011
+ return targetFile;
1012
+ };
1013
+
1014
+ /**
1015
+ * Handle /remember <fact> — append fact to NIMBUS.md Agent Memory. G17
1016
+ */
1017
+ const onRemember: import('../App').OnRememberCallback = async (fact: string): Promise<void> => {
1018
+ // Find the NIMBUS.md path in use
1019
+ const nimbusMdPath = nimbusMdPaths.find(p => {
1020
+ try { return existsSync(p); } catch { return false; }
1021
+ }) ?? nimbusMdPaths[0];
1022
+
1023
+ let content = '';
1024
+ try {
1025
+ if (existsSync(nimbusMdPath)) {
1026
+ content = readFileSync(nimbusMdPath, 'utf-8');
1027
+ }
1028
+ } catch { /* will create new */ }
1029
+
1030
+ const MEMORY_SECTION = '## Agent Memory';
1031
+ if (content.includes(MEMORY_SECTION)) {
1032
+ // Append to existing section
1033
+ content = content.replace(
1034
+ new RegExp(`(${MEMORY_SECTION}[\\s\\S]*?)(?=\\n##|$)`),
1035
+ `$1\n- ${fact}`
1036
+ );
1037
+ } else {
1038
+ content += `\n${MEMORY_SECTION}\n\n- ${fact}\n`;
1039
+ }
1040
+
1041
+ const { writeFileSync, mkdirSync } = await import('node:fs');
1042
+ const { dirname } = await import('node:path');
1043
+ mkdirSync(dirname(nimbusMdPath), { recursive: true });
1044
+ writeFileSync(nimbusMdPath, content, 'utf-8');
1045
+ // Reload instructions
1046
+ nimbusInstructions = content;
1047
+ };
1048
+
561
1049
  /**
562
1050
  * Handle /sessions command — list active sessions.
563
1051
  */
@@ -567,13 +1055,37 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
567
1055
  }
568
1056
  try {
569
1057
  const sessions = sessionManager.list();
570
- return sessions.map(s => ({
571
- id: s.id,
572
- name: s.name ?? `session-${s.id.slice(0, 8)}`,
573
- model: s.model ?? 'default',
574
- mode: s.mode ?? 'build',
575
- updatedAt: s.updatedAt ?? new Date().toISOString(),
576
- }));
1058
+ // L9: include token and cost summary
1059
+ let totalTokens = 0;
1060
+ let totalCost = 0;
1061
+ const mapped: SessionSummary[] = sessions.map(s => {
1062
+ const tokens = (s as unknown as Record<string, unknown>).tokenCount as number | undefined ?? 0;
1063
+ const cost = (s as unknown as Record<string, unknown>).costUSD as number | undefined ?? 0;
1064
+ totalTokens += tokens;
1065
+ totalCost += cost;
1066
+ return {
1067
+ id: s.id,
1068
+ name: s.name ?? `session-${s.id.slice(0, 8)}`,
1069
+ model: s.model ?? 'default',
1070
+ mode: (s.mode ?? 'build') as string,
1071
+ updatedAt: s.updatedAt ?? new Date().toISOString(),
1072
+ tokenCount: tokens,
1073
+ costUSD: cost,
1074
+ };
1075
+ });
1076
+ // Append a total row as a synthetic session entry
1077
+ if (mapped.length > 0) {
1078
+ mapped.push({
1079
+ id: '__total__',
1080
+ name: `Total (${mapped.length} sessions)`,
1081
+ model: '',
1082
+ mode: '',
1083
+ updatedAt: '',
1084
+ tokenCount: totalTokens,
1085
+ costUSD: totalCost,
1086
+ });
1087
+ }
1088
+ return mapped;
577
1089
  } catch {
578
1090
  return [];
579
1091
  }
@@ -655,7 +1167,7 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
655
1167
  };
656
1168
 
657
1169
  // Convert restored LLM history into UIMessages for the TUI
658
- const initialMessages: UIMessage[] = history
1170
+ const restoredMessages: UIMessage[] = history
659
1171
  .filter(m => m.role === 'user' || m.role === 'assistant')
660
1172
  .map(m => ({
661
1173
  id: crypto.randomUUID(),
@@ -664,11 +1176,136 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
664
1176
  timestamp: new Date(),
665
1177
  }));
666
1178
 
1179
+ // Show a welcome message on fresh sessions (no prior history)
1180
+ const isNewSession = restoredMessages.length === 0;
1181
+ // L4: Check for prior sessions to show resume hint
1182
+ let priorSessionCount = 0;
1183
+ try {
1184
+ if (sessionManager) {
1185
+ const allSessions = sessionManager.list();
1186
+ priorSessionCount = allSessions.filter(s => s.id !== sessionId).length;
1187
+ }
1188
+ } catch { /* non-critical */ }
1189
+ const welcomeMessage: UIMessage | null = isNewSession
1190
+ ? (() => {
1191
+ // G10: DevOps-context-aware welcome message
1192
+ const infraLines: string[] = [];
1193
+ if (currentInfraContext?.kubectlContext) {
1194
+ infraLines.push(` Kubernetes: ${currentInfraContext.kubectlContext}`);
1195
+ }
1196
+ if (currentInfraContext?.terraformWorkspace) {
1197
+ infraLines.push(` Terraform: workspace=${currentInfraContext.terraformWorkspace}`);
1198
+ }
1199
+ if (currentInfraContext?.awsAccount) {
1200
+ infraLines.push(` AWS: ${currentInfraContext.awsAccount}${currentInfraContext.awsRegion ? ` / ${currentInfraContext.awsRegion}` : ''}`);
1201
+ }
1202
+ if (currentInfraContext?.gcpProject) {
1203
+ infraLines.push(` GCP: ${currentInfraContext.gcpProject}`);
1204
+ }
1205
+
1206
+ // GAP-17: context-aware suggestions based on detected infrastructure
1207
+ const suggestions: string[] = [];
1208
+ if (currentInfraContext?.terraformWorkspace) suggestions.push(`"check for drift in workspace ${currentInfraContext.terraformWorkspace}"`);
1209
+ if (currentInfraContext?.kubectlContext) suggestions.push(`"show all pods in ${currentInfraContext.kubectlContext}"`);
1210
+ if (currentInfraContext?.awsAccount) suggestions.push(`"show AWS costs for this month"`);
1211
+ if ((currentInfraContext?.helmReleases?.length ?? 0) > 0) suggestions.push(`"show helm release history for ${currentInfraContext!.helmReleases![0]}"`);
1212
+
1213
+ // H5: Build one-line infra hint for cold start
1214
+ const infraHintParts: string[] = [];
1215
+ if (currentInfraContext?.terraformWorkspace) infraHintParts.push(`tf:${currentInfraContext.terraformWorkspace}`);
1216
+ if (currentInfraContext?.kubectlContext) infraHintParts.push(`k8s:${currentInfraContext.kubectlContext}`);
1217
+ if (currentInfraContext?.awsAccount) infraHintParts.push(`aws:${currentInfraContext.awsAccount}`);
1218
+ if (currentInfraContext?.gcpProject) infraHintParts.push(`gcp:${currentInfraContext.gcpProject}`);
1219
+ if ((currentInfraContext?.helmReleases?.length ?? 0) > 0) infraHintParts.push(`${currentInfraContext!.helmReleases!.length} helm release${currentInfraContext!.helmReleases!.length > 1 ? 's' : ''}`);
1220
+ const infraHintLine = infraHintParts.length > 0 ? `Infra detected: ${infraHintParts.join(' | ')}` : '';
1221
+
1222
+ // G24: DevOps-specific quick-start examples
1223
+ // M3: When no NIMBUS.md, show concrete DevOps prompt examples to reduce blank-prompt friction
1224
+ const noNimbusHints = !nimbusInstructions ? [
1225
+ '',
1226
+ 'Try asking:',
1227
+ ' "list my kubernetes pods in the staging namespace"',
1228
+ ' "run terraform plan in ./infrastructure"',
1229
+ ' "show me the helm releases and their status"',
1230
+ ' "check for infrastructure drift"',
1231
+ ] : [];
1232
+ const content = [
1233
+ 'Welcome to Nimbus — Your AI DevOps Operator.',
1234
+ ...(infraHintLine ? ['', infraHintLine] : []),
1235
+ '',
1236
+ ...(infraLines.length > 0 ? ['Detected infrastructure:', ...infraLines, ''] : []),
1237
+ ...(suggestions.length > 0 ? ['', 'Suggested:', ...suggestions.map(s => ` • ${s}`)] : []),
1238
+ ...noNimbusHints,
1239
+ '',
1240
+ 'Mode: PLAN (read-only). Tab → build → deploy to escalate.',
1241
+ '',
1242
+ 'Quick-start examples:',
1243
+ ' "Show me all failing pods across all namespaces"',
1244
+ ' "What terraform changes are pending in the staging workspace?"',
1245
+ ' "Check for infrastructure drift between actual and desired state"',
1246
+ ' "Summarize last 24 hours of production incidents in PagerDuty"',
1247
+ '',
1248
+ '/k8s-ctx — switch cluster /tf-ws — switch workspace',
1249
+ '/help — all commands Tab — cycle modes',
1250
+ '',
1251
+ nimbusInstructions
1252
+ ? 'NIMBUS.md loaded — project context active.'
1253
+ : 'Tip: run `nimbus init` to generate a NIMBUS.md with your infra context.',
1254
+ // L4: Session resume hint
1255
+ ...(priorSessionCount > 0
1256
+ ? ['', 'Previous session available — type /sessions to resume or /new to start fresh.']
1257
+ : []),
1258
+ ].join('\n');
1259
+
1260
+ return {
1261
+ id: crypto.randomUUID(),
1262
+ role: 'system' as const,
1263
+ content,
1264
+ timestamp: new Date(),
1265
+ };
1266
+ })()
1267
+ : null;
1268
+
1269
+ // Gap 19: append any startup warnings as a system message
1270
+ const startupWarningMessages: UIMessage[] = _startupWarnings.length > 0
1271
+ ? [{
1272
+ id: crypto.randomUUID(),
1273
+ role: 'system' as const,
1274
+ content: `Startup warnings:\n${_startupWarnings.map(w => ` ⚠ ${w}`).join('\n')}`,
1275
+ timestamp: new Date(),
1276
+ }]
1277
+ : [];
1278
+
1279
+ // G4: Proactive NIMBUS.md banner when auto-init failed to create one
1280
+ const nimbusMdBannerMessage: UIMessage | null = showNimbusMdBanner ? {
1281
+ id: crypto.randomUUID(),
1282
+ role: 'system' as const,
1283
+ content: [
1284
+ '**No NIMBUS.md found in this directory.**',
1285
+ '',
1286
+ 'Type `/init` to auto-generate project context — I\'ll detect your Terraform workspaces,',
1287
+ 'Kubernetes clusters, AWS accounts, and more.',
1288
+ '',
1289
+ 'Or ask me anything directly. I work best with project context loaded.',
1290
+ ].join('\n'),
1291
+ timestamp: new Date(),
1292
+ } : null;
1293
+
1294
+ const initialMessages: UIMessage[] = [
1295
+ ...(welcomeMessage ? [welcomeMessage] : []),
1296
+ ...(nimbusMdBannerMessage ? [nimbusMdBannerMessage] : []),
1297
+ ...(resumeContextMessage ? [resumeContextMessage] : []),
1298
+ ...startupWarningMessages,
1299
+ ...restoredMessages,
1300
+ ];
1301
+
667
1302
  // Build props for the App component
668
1303
  const appProps: AppProps = {
669
1304
  initialSession: {
670
1305
  model: options.model ?? 'default',
671
1306
  mode: currentMode,
1307
+ kubectlContext: currentInfraContext?.kubectlContext,
1308
+ terraformWorkspace: currentInfraContext?.terraformWorkspace,
672
1309
  },
673
1310
  initialMessages: initialMessages.length > 0 ? initialMessages : undefined,
674
1311
  onMessage,
@@ -681,22 +1318,107 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
681
1318
  onClear,
682
1319
  onModelChange,
683
1320
  onModeChange,
1321
+ onDiff,
1322
+ onCost,
1323
+ onInit,
1324
+ onExport,
1325
+ onRemember,
684
1326
  onSessions,
685
1327
  onNewSession,
686
1328
  onSwitchSession,
1329
+ onFetchCompletions: async (prefix: string): Promise<string[]> => {
1330
+ // H3: Fetch dynamic completions for slash command arguments (cached 30s in InputBox)
1331
+ try {
1332
+ const { execFile } = await import('node:child_process');
1333
+ const { promisify } = await import('node:util');
1334
+ const execFileAsync = promisify(execFile);
1335
+
1336
+ if (prefix.startsWith('/k8s-ctx ')) {
1337
+ const { stdout } = await execFileAsync('kubectl', ['config', 'get-contexts', '-o', 'name'], { timeout: 5000 });
1338
+ return stdout.trim().split('\n').filter(Boolean);
1339
+ }
1340
+ if (prefix.startsWith('/tf-ws ')) {
1341
+ const { stdout } = await execFileAsync('terraform', ['workspace', 'list'], { timeout: 10000, cwd: process.cwd() });
1342
+ return stdout.trim().split('\n').map(l => l.replace(/^\*?\s+/, '')).filter(Boolean);
1343
+ }
1344
+ if (prefix.startsWith('/model ')) {
1345
+ const modelsMap = await ctx.router.getAvailableModels();
1346
+ return Object.values(modelsMap).flat();
1347
+ }
1348
+ if (prefix.startsWith('/profile ')) {
1349
+ const { listProfiles } = await import('../../config/profiles');
1350
+ return listProfiles();
1351
+ }
1352
+ } catch { /* non-critical */ }
1353
+ return [];
1354
+ },
687
1355
  onReady: imperativeApi => {
688
1356
  api = imperativeApi;
1357
+ // GAP-2: Fire background LLM connectivity check after API is ready
1358
+ api.setLLMHealth('checking');
1359
+ (async () => {
1360
+ try {
1361
+ const providers = await ctx.router.getAvailableProviders();
1362
+ if (providers.length > 0) {
1363
+ api!.setLLMHealth('ok');
1364
+ } else {
1365
+ api!.setLLMHealth('error');
1366
+ }
1367
+ } catch {
1368
+ api!.setLLMHealth('error');
1369
+ }
1370
+ })();
689
1371
  },
690
1372
  };
691
1373
 
692
1374
  // Render the Ink application wrapped in an error boundary
693
- const { waitUntilExit } = render(
694
- React.createElement(AppErrorBoundary, null, React.createElement(App, appProps))
1375
+ const inkInstance = render(
1376
+ React.createElement(AppErrorBoundary, null, React.createElement(App, { ...appProps, columns: process.stdout.columns ?? 80 }))
695
1377
  );
1378
+ const { waitUntilExit } = inkInstance;
1379
+
1380
+ // C1: Re-render on terminal resize so Ink layout reflows correctly
1381
+ const handleResize = () => {
1382
+ try {
1383
+ inkInstance.rerender(
1384
+ React.createElement(AppErrorBoundary, null, React.createElement(App, { ...appProps, columns: process.stdout.columns ?? 80 }))
1385
+ );
1386
+ } catch { /* non-critical */ }
1387
+ };
1388
+ process.stdout.on('resize', handleResize);
1389
+ process.on('SIGWINCH', handleResize);
696
1390
 
697
- // When the TUI exits, clean up watcher and mark session as completed
1391
+ // Gap 16: Periodic cloud auth status check every 15 minutes
1392
+ const authCheckInterval = setInterval(async () => {
1393
+ try {
1394
+ const { execFile } = await import('node:child_process');
1395
+ const { promisify } = await import('node:util');
1396
+ const execFileAsync = promisify(execFile);
1397
+ const expired: string[] = [];
1398
+
1399
+ // Check AWS
1400
+ try {
1401
+ await execFileAsync('aws', ['sts', 'get-caller-identity'], { timeout: 5000 });
1402
+ } catch {
1403
+ expired.push('AWS');
1404
+ }
1405
+
1406
+ if (expired.length > 0) {
1407
+ addMessage({
1408
+ id: crypto.randomUUID(),
1409
+ role: 'system',
1410
+ content: `Cloud credentials may have expired: ${expired.join(', ')}. Run /auth-refresh to renew.`,
1411
+ timestamp: new Date(),
1412
+ });
1413
+ }
1414
+ } catch { /* non-critical */ }
1415
+ }, 15 * 60 * 1000);
1416
+
1417
+ // When the TUI exits, clean up watcher, LSP servers, and mark session as completed
698
1418
  process.on('exit', () => {
1419
+ clearInterval(authCheckInterval);
699
1420
  watcher.stop();
1421
+ lspManager.stopAll();
700
1422
  if (sessionManager && sessionId) {
701
1423
  try {
702
1424
  sessionManager.complete(sessionId);
@@ -704,8 +1426,19 @@ export async function startInkChat(options: InkChatOptions = {}): Promise<void>
704
1426
  /* ignore */
705
1427
  }
706
1428
  }
1429
+ // H1: Persist final infra context on exit so next session starts with it
1430
+ if (currentInfraContext) {
1431
+ try {
1432
+ writeFileSync(infraStatePath, JSON.stringify(currentInfraContext, null, 2), 'utf-8');
1433
+ } catch { /* non-critical */ }
1434
+ }
707
1435
  });
708
1436
 
709
1437
  // Keep the process alive until the user exits (Ctrl+C twice, or exit())
710
1438
  await waitUntilExit();
1439
+
1440
+ // A7: Session saved hint on exit
1441
+ if (sessionId && process.stderr.isTTY) {
1442
+ process.stderr.write('\n\x1b[2mSession saved. Resume with: nimbus chat --continue\x1b[0m\n');
1443
+ }
711
1444
  }