@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.
- package/bin/nimbus +26 -10
- package/bin/nimbus.cmd +41 -0
- package/bin/nimbus.mjs +70 -0
- package/completions/nimbus.bash +38 -0
- package/completions/nimbus.fish +48 -0
- package/completions/nimbus.zsh +81 -0
- package/dist/src/agent/compaction-agent.js +215 -0
- package/dist/src/agent/context-manager.js +385 -0
- package/dist/src/agent/context.js +322 -0
- package/dist/src/agent/deploy-preview.js +395 -0
- package/dist/src/agent/expand-files.js +95 -0
- package/dist/src/agent/index.js +18 -0
- package/dist/src/agent/loop.js +1535 -0
- package/dist/src/agent/modes.js +347 -0
- package/dist/src/agent/permissions.js +396 -0
- package/dist/src/agent/subagents/base.js +67 -0
- package/dist/src/agent/subagents/cost.js +45 -0
- package/dist/src/agent/subagents/explore.js +36 -0
- package/dist/src/agent/subagents/general.js +41 -0
- package/dist/src/agent/subagents/index.js +88 -0
- package/dist/src/agent/subagents/infra.js +52 -0
- package/dist/src/agent/subagents/security.js +60 -0
- package/dist/src/agent/system-prompt.js +860 -0
- package/dist/src/app.js +152 -0
- package/dist/src/audit/activity-log.js +209 -0
- package/dist/src/audit/compliance-checker.js +419 -0
- package/dist/src/audit/cost-tracker.js +231 -0
- package/dist/src/audit/index.js +10 -0
- package/dist/src/audit/security-scanner.js +490 -0
- package/dist/src/auth/guard.js +64 -0
- package/dist/src/auth/index.js +19 -0
- package/dist/src/auth/keychain.js +79 -0
- package/dist/src/auth/oauth.js +389 -0
- package/dist/src/auth/providers.js +415 -0
- package/dist/src/auth/sso.js +87 -0
- package/dist/src/auth/store.js +424 -0
- package/dist/src/auth/types.js +5 -0
- package/dist/src/cli/index.js +8 -0
- package/dist/src/cli/init.js +1048 -0
- package/dist/src/cli/openapi-spec.js +346 -0
- package/dist/src/cli/run.js +505 -0
- package/dist/src/cli/serve-auth.js +56 -0
- package/dist/src/cli/serve.js +432 -0
- package/dist/src/cli/web.js +50 -0
- package/dist/src/cli.js +1574 -0
- package/dist/src/clients/core-engine-client.js +156 -0
- package/dist/src/clients/enterprise-client.js +246 -0
- package/dist/src/clients/generator-client.js +219 -0
- package/dist/src/clients/git-client.js +367 -0
- package/dist/src/clients/github-client.js +229 -0
- package/dist/src/clients/helm-client.js +299 -0
- package/dist/src/clients/index.js +18 -0
- package/dist/src/clients/k8s-client.js +270 -0
- package/dist/src/clients/llm-client.js +119 -0
- package/dist/src/clients/rest-client.js +104 -0
- package/dist/src/clients/service-discovery.js +35 -0
- package/dist/src/clients/terraform-client.js +302 -0
- package/dist/src/clients/tools-client.js +1227 -0
- package/dist/src/clients/ws-client.js +93 -0
- package/dist/src/commands/alias.js +91 -0
- package/dist/src/commands/analyze/index.js +313 -0
- package/dist/src/commands/apply/helm.js +375 -0
- package/dist/src/commands/apply/index.js +176 -0
- package/dist/src/commands/apply/k8s.js +350 -0
- package/dist/src/commands/apply/terraform.js +465 -0
- package/dist/src/commands/ask.js +137 -0
- package/dist/src/commands/audit/index.js +322 -0
- package/dist/src/commands/auth-cloud.js +345 -0
- package/dist/src/commands/auth-list.js +112 -0
- package/dist/src/commands/auth-profile.js +104 -0
- package/dist/src/commands/auth-refresh.js +161 -0
- package/dist/src/commands/auth-status.js +122 -0
- package/dist/src/commands/aws/ec2.js +402 -0
- package/dist/src/commands/aws/iam.js +304 -0
- package/dist/src/commands/aws/index.js +108 -0
- package/dist/src/commands/aws/lambda.js +317 -0
- package/dist/src/commands/aws/rds.js +345 -0
- package/dist/src/commands/aws/s3.js +346 -0
- package/dist/src/commands/aws/vpc.js +302 -0
- package/dist/src/commands/aws-discover.js +413 -0
- package/dist/src/commands/aws-terraform.js +618 -0
- package/dist/src/commands/azure/aks.js +305 -0
- package/dist/src/commands/azure/functions.js +200 -0
- package/dist/src/commands/azure/index.js +93 -0
- package/dist/src/commands/azure/storage.js +378 -0
- package/dist/src/commands/azure/vm.js +291 -0
- package/dist/src/commands/billing/index.js +224 -0
- package/dist/src/commands/chat.js +259 -0
- package/dist/src/commands/completions.js +255 -0
- package/dist/src/commands/config.js +291 -0
- package/dist/src/commands/cost/cloud-cost-estimator.js +211 -0
- package/dist/src/commands/cost/estimator.js +73 -0
- package/dist/src/commands/cost/index.js +625 -0
- package/dist/src/commands/cost/parsers/terraform.js +234 -0
- package/dist/src/commands/cost/parsers/types.js +4 -0
- package/dist/src/commands/cost/pricing/aws.js +501 -0
- package/dist/src/commands/cost/pricing/azure.js +462 -0
- package/dist/src/commands/cost/pricing/gcp.js +359 -0
- package/dist/src/commands/cost/pricing/index.js +24 -0
- package/dist/src/commands/demo.js +196 -0
- package/dist/src/commands/deploy.js +215 -0
- package/dist/src/commands/doctor.js +1291 -0
- package/dist/src/commands/drift/index.js +674 -0
- package/dist/src/commands/explain.js +235 -0
- package/dist/src/commands/export.js +120 -0
- package/dist/src/commands/feedback.js +319 -0
- package/dist/src/commands/fix.js +263 -0
- package/dist/src/commands/fs/index.js +338 -0
- package/dist/src/commands/gcp/compute.js +266 -0
- package/dist/src/commands/gcp/functions.js +221 -0
- package/dist/src/commands/gcp/gke.js +357 -0
- package/dist/src/commands/gcp/iam.js +295 -0
- package/dist/src/commands/gcp/index.js +105 -0
- package/dist/src/commands/gcp/storage.js +232 -0
- package/dist/src/commands/generate-helm.js +1026 -0
- package/dist/src/commands/generate-k8s.js +1263 -0
- package/dist/src/commands/generate-terraform.js +1058 -0
- package/dist/src/commands/gh/index.js +663 -0
- package/dist/src/commands/git/index.js +1208 -0
- package/dist/src/commands/helm/index.js +985 -0
- package/dist/src/commands/help.js +639 -0
- package/dist/src/commands/history.js +120 -0
- package/dist/src/commands/import.js +782 -0
- package/dist/src/commands/incident.js +144 -0
- package/dist/src/commands/index.js +109 -0
- package/dist/src/commands/init.js +955 -0
- package/dist/src/commands/k8s/index.js +979 -0
- package/dist/src/commands/login.js +588 -0
- package/dist/src/commands/logout.js +61 -0
- package/dist/src/commands/logs.js +160 -0
- package/dist/src/commands/onboarding.js +382 -0
- package/dist/src/commands/pipeline.js +153 -0
- package/dist/src/commands/plan/display.js +216 -0
- package/dist/src/commands/plan/index.js +525 -0
- package/dist/src/commands/plugin.js +325 -0
- package/dist/src/commands/preview.js +356 -0
- package/dist/src/commands/profile.js +297 -0
- package/dist/src/commands/questionnaire.js +1021 -0
- package/dist/src/commands/resume.js +35 -0
- package/dist/src/commands/rollback.js +259 -0
- package/dist/src/commands/rollout.js +74 -0
- package/dist/src/commands/runbook.js +307 -0
- package/dist/src/commands/schedule.js +202 -0
- package/dist/src/commands/status.js +213 -0
- package/dist/src/commands/team/index.js +309 -0
- package/dist/src/commands/team-context.js +200 -0
- package/dist/src/commands/template.js +204 -0
- package/dist/src/commands/tf/index.js +989 -0
- package/dist/src/commands/upgrade.js +515 -0
- package/dist/src/commands/usage/index.js +118 -0
- package/dist/src/commands/version.js +145 -0
- package/dist/src/commands/watch.js +127 -0
- package/dist/src/compat/index.js +2 -0
- package/dist/src/compat/runtime.js +10 -0
- package/dist/src/compat/sqlite.js +144 -0
- package/dist/src/config/index.js +6 -0
- package/dist/src/config/manager.js +469 -0
- package/dist/src/config/mode-store.js +57 -0
- package/dist/src/config/profiles.js +66 -0
- package/dist/src/config/safety-policy.js +251 -0
- package/dist/src/config/schema.js +107 -0
- package/dist/src/config/types.js +311 -0
- package/dist/src/config/workspace-state.js +38 -0
- package/dist/src/context/context-db.js +138 -0
- package/dist/src/demo/index.js +295 -0
- package/dist/src/demo/scenarios/full-journey.js +226 -0
- package/dist/src/demo/scenarios/getting-started.js +124 -0
- package/dist/src/demo/scenarios/helm-release.js +334 -0
- package/dist/src/demo/scenarios/k8s-deployment.js +190 -0
- package/dist/src/demo/scenarios/terraform-vpc.js +167 -0
- package/dist/src/demo/types.js +6 -0
- package/dist/src/engine/cost-estimator.js +334 -0
- package/dist/src/engine/diagram-generator.js +192 -0
- package/dist/src/engine/drift-detector.js +688 -0
- package/dist/src/engine/executor.js +832 -0
- package/dist/src/engine/index.js +39 -0
- package/dist/src/engine/orchestrator.js +436 -0
- package/dist/src/engine/planner.js +616 -0
- package/dist/src/engine/safety.js +609 -0
- package/dist/src/engine/verifier.js +664 -0
- package/dist/src/enterprise/audit.js +241 -0
- package/dist/src/enterprise/auth.js +189 -0
- package/dist/src/enterprise/billing.js +512 -0
- package/dist/src/enterprise/index.js +16 -0
- package/dist/src/enterprise/teams.js +315 -0
- package/dist/src/generator/best-practices.js +1375 -0
- package/dist/src/generator/helm.js +495 -0
- package/dist/src/generator/index.js +11 -0
- package/dist/src/generator/intent-parser.js +420 -0
- package/dist/src/generator/kubernetes.js +773 -0
- package/dist/src/generator/terraform.js +1472 -0
- package/dist/src/history/index.js +6 -0
- package/dist/src/history/manager.js +199 -0
- package/dist/src/history/types.js +6 -0
- package/dist/src/hooks/config.js +318 -0
- package/dist/src/hooks/engine.js +317 -0
- package/dist/src/hooks/index.js +2 -0
- package/dist/src/llm/auth-bridge.js +157 -0
- package/dist/src/llm/circuit-breaker.js +116 -0
- package/dist/src/llm/config-loader.js +172 -0
- package/dist/src/llm/cost-calculator.js +137 -0
- package/dist/src/llm/index.js +7 -0
- package/dist/src/llm/model-aliases.js +99 -0
- package/dist/src/llm/provider-registry.js +57 -0
- package/dist/src/llm/providers/anthropic.js +430 -0
- package/dist/src/llm/providers/bedrock.js +409 -0
- package/dist/src/llm/providers/google.js +344 -0
- package/dist/src/llm/providers/ollama.js +661 -0
- package/dist/src/llm/providers/openai-compatible.js +289 -0
- package/dist/src/llm/providers/openai.js +284 -0
- package/dist/src/llm/providers/openrouter.js +293 -0
- package/dist/src/llm/router.js +844 -0
- package/dist/src/llm/types.js +69 -0
- package/dist/src/lsp/client.js +239 -0
- package/dist/src/lsp/languages.js +95 -0
- package/dist/src/lsp/manager.js +243 -0
- package/dist/src/mcp/client.js +289 -0
- package/dist/src/mcp/index.js +5 -0
- package/dist/src/mcp/manager.js +113 -0
- package/dist/src/nimbus.js +212 -0
- package/dist/src/plugins/index.js +13 -0
- package/dist/src/plugins/loader.js +280 -0
- package/dist/src/plugins/manager.js +282 -0
- package/dist/src/plugins/types.js +23 -0
- package/dist/src/scanners/cicd-scanner.js +230 -0
- package/dist/src/scanners/cloud-scanner.js +415 -0
- package/dist/src/scanners/framework-scanner.js +430 -0
- package/dist/src/scanners/iac-scanner.js +350 -0
- package/dist/src/scanners/index.js +454 -0
- package/dist/src/scanners/language-scanner.js +258 -0
- package/dist/src/scanners/package-manager-scanner.js +252 -0
- package/dist/src/scanners/types.js +6 -0
- package/dist/src/sessions/manager.js +395 -0
- package/dist/src/sessions/types.js +4 -0
- package/dist/src/sharing/sync.js +238 -0
- package/dist/src/sharing/viewer.js +131 -0
- package/dist/src/snapshots/index.js +1 -0
- package/dist/src/snapshots/manager.js +432 -0
- package/dist/src/state/artifacts.js +94 -0
- package/dist/src/state/audit.js +73 -0
- package/dist/src/state/billing.js +126 -0
- package/dist/src/state/checkpoints.js +81 -0
- package/dist/src/state/config.js +58 -0
- package/dist/src/state/conversations.js +7 -0
- package/dist/src/state/credentials.js +96 -0
- package/dist/src/state/db.js +53 -0
- package/dist/src/state/index.js +23 -0
- package/dist/src/state/messages.js +76 -0
- package/dist/src/state/projects.js +92 -0
- package/dist/src/state/schema.js +233 -0
- package/dist/src/state/sessions.js +79 -0
- package/dist/src/state/teams.js +131 -0
- package/dist/src/telemetry.js +91 -0
- package/dist/src/tools/aws-ops.js +747 -0
- package/dist/src/tools/azure-ops.js +491 -0
- package/dist/src/tools/file-ops.js +451 -0
- package/dist/src/tools/gcp-ops.js +559 -0
- package/dist/src/tools/git-ops.js +557 -0
- package/dist/src/tools/github-ops.js +460 -0
- package/dist/src/tools/helm-ops.js +634 -0
- package/dist/src/tools/index.js +16 -0
- package/dist/src/tools/k8s-ops.js +579 -0
- package/dist/src/tools/schemas/converter.js +129 -0
- package/dist/src/tools/schemas/devops.js +3319 -0
- package/dist/src/tools/schemas/index.js +19 -0
- package/dist/src/tools/schemas/standard.js +966 -0
- package/dist/src/tools/schemas/types.js +409 -0
- package/dist/src/tools/spawn-exec.js +109 -0
- package/dist/src/tools/terraform-ops.js +627 -0
- package/dist/src/types/config.js +1 -0
- package/dist/src/types/drift.js +4 -0
- package/dist/src/types/enterprise.js +5 -0
- package/dist/src/types/index.js +14 -0
- package/dist/src/types/plan.js +1 -0
- package/dist/src/types/request.js +1 -0
- package/dist/src/types/response.js +1 -0
- package/dist/src/types/service.js +1 -0
- package/dist/src/ui/App.js +1672 -0
- package/dist/src/ui/DeployPreview.js +60 -0
- package/dist/src/ui/FileDiffModal.js +108 -0
- package/dist/src/ui/Header.js +46 -0
- package/dist/src/ui/HelpModal.js +9 -0
- package/dist/src/ui/InputBox.js +408 -0
- package/dist/src/ui/MessageList.js +795 -0
- package/dist/src/ui/PermissionPrompt.js +72 -0
- package/dist/src/ui/StatusBar.js +109 -0
- package/dist/src/ui/TerminalPane.js +31 -0
- package/dist/src/ui/ToolCallDisplay.js +303 -0
- package/dist/src/ui/TreePane.js +83 -0
- package/dist/src/ui/chat-ui.js +721 -0
- package/dist/src/ui/index.js +11 -0
- package/dist/src/ui/ink/index.js +1325 -0
- package/dist/src/ui/streaming.js +137 -0
- package/dist/src/ui/theme.js +78 -0
- package/dist/src/ui/types.js +7 -0
- package/dist/src/utils/analytics.js +61 -0
- package/dist/src/utils/cost-warning.js +25 -0
- package/dist/src/utils/env.js +42 -0
- package/dist/src/utils/errors.js +54 -0
- package/dist/src/utils/event-bus.js +22 -0
- package/dist/src/utils/index.js +16 -0
- package/dist/src/utils/logger.js +150 -0
- package/dist/src/utils/rate-limiter.js +90 -0
- package/dist/src/utils/service-auth.js +36 -0
- package/dist/src/utils/validation.js +39 -0
- package/dist/src/version.js +3 -0
- package/dist/src/watcher/index.js +192 -0
- package/dist/src/wizard/approval.js +275 -0
- package/dist/src/wizard/index.js +13 -0
- package/dist/src/wizard/prompts.js +273 -0
- package/dist/src/wizard/types.js +4 -0
- package/dist/src/wizard/ui.js +453 -0
- package/dist/src/wizard/wizard.js +227 -0
- package/package.json +31 -23
- package/src/__tests__/alias.test.ts +133 -0
- package/src/__tests__/app.test.ts +1 -1
- package/src/__tests__/audit.test.ts +1 -1
- package/src/__tests__/circuit-breaker.test.ts +1 -1
- package/src/__tests__/cli-run.test.ts +237 -1
- package/src/__tests__/compat-sqlite.test.ts +68 -0
- package/src/__tests__/context-manager.test.ts +131 -1
- package/src/__tests__/context.test.ts +1 -1
- package/src/__tests__/devops-terminal-gaps.test.ts +718 -0
- package/src/__tests__/doctor.test.ts +48 -0
- package/src/__tests__/enterprise.test.ts +1 -1
- package/src/__tests__/export.test.ts +236 -0
- package/src/__tests__/gap-11-18-20.test.ts +958 -0
- package/src/__tests__/generator.test.ts +1 -1
- package/src/__tests__/helm-streaming.test.ts +127 -0
- package/src/__tests__/hooks.test.ts +1 -1
- package/src/__tests__/incident.test.ts +179 -0
- package/src/__tests__/init.test.ts +55 -4
- package/src/__tests__/intent-parser.test.ts +1 -1
- package/src/__tests__/llm-router.test.ts +1 -1
- package/src/__tests__/logs.test.ts +107 -0
- package/src/__tests__/loop-errors.test.ts +244 -0
- package/src/__tests__/lsp.test.ts +1 -1
- package/src/__tests__/modes.test.ts +1 -1
- package/src/__tests__/perf-optimizations.test.ts +847 -0
- package/src/__tests__/permissions.test.ts +1 -1
- package/src/__tests__/pipeline.test.ts +50 -0
- package/src/__tests__/polish-phase3.test.ts +340 -0
- package/src/__tests__/profile.test.ts +237 -0
- package/src/__tests__/rollback.test.ts +83 -0
- package/src/__tests__/runbook.test.ts +219 -0
- package/src/__tests__/schedule.test.ts +206 -0
- package/src/__tests__/serve.test.ts +1 -1
- package/src/__tests__/sessions.test.ts +96 -1
- package/src/__tests__/sharing.test.ts +53 -1
- package/src/__tests__/snapshots.test.ts +1 -1
- package/src/__tests__/standalone-migration.test.ts +199 -0
- package/src/__tests__/state-db.test.ts +1 -1
- package/src/__tests__/status.test.ts +158 -0
- package/src/__tests__/stream-with-tools.test.ts +71 -25
- package/src/__tests__/subagents.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +82 -3
- package/src/__tests__/terminal-gap-v2.test.ts +395 -0
- package/src/__tests__/terminal-parity.test.ts +393 -0
- package/src/__tests__/tf-apply.test.ts +187 -0
- package/src/__tests__/tool-converter.test.ts +1 -1
- package/src/__tests__/tool-schemas.test.ts +209 -4
- package/src/__tests__/tools.test.ts +4 -3
- package/src/__tests__/version-json.test.ts +184 -0
- package/src/__tests__/version.test.ts +1 -1
- package/src/__tests__/watch.test.ts +129 -0
- package/src/agent/compaction-agent.ts +40 -1
- package/src/agent/context-manager.ts +67 -3
- package/src/agent/deploy-preview.ts +62 -1
- package/src/agent/expand-files.ts +108 -0
- package/src/agent/loop.ts +1312 -31
- package/src/agent/permissions.ts +51 -4
- package/src/agent/system-prompt.ts +573 -19
- package/src/app.ts +58 -0
- package/src/audit/security-scanner.ts +45 -0
- package/src/auth/keychain.ts +82 -0
- package/src/auth/oauth.ts +15 -5
- package/src/cli/init.ts +378 -5
- package/src/cli/run.ts +407 -16
- package/src/cli/serve.ts +78 -1
- package/src/cli/web.ts +10 -6
- package/src/cli.ts +312 -1
- package/src/clients/service-discovery.ts +30 -25
- package/src/commands/alias.ts +100 -0
- package/src/commands/audit/index.ts +121 -2
- package/src/commands/auth-cloud.ts +113 -0
- package/src/commands/auth-refresh.ts +187 -0
- package/src/commands/aws-discover.ts +144 -251
- package/src/commands/aws-terraform.ts +68 -118
- package/src/commands/chat.ts +9 -3
- package/src/commands/completions.ts +268 -0
- package/src/commands/config.ts +26 -0
- package/src/commands/cost/index.ts +218 -2
- package/src/commands/deploy.ts +260 -0
- package/src/commands/doctor.ts +744 -152
- package/src/commands/drift/index.ts +371 -23
- package/src/commands/export.ts +146 -0
- package/src/commands/generate-k8s.ts +9 -61
- package/src/commands/generate-terraform.ts +191 -449
- package/src/commands/help.ts +212 -36
- package/src/commands/history.ts +8 -1
- package/src/commands/incident.ts +166 -0
- package/src/commands/init.ts +5 -0
- package/src/commands/login.ts +86 -1
- package/src/commands/logs.ts +167 -0
- package/src/commands/onboarding.ts +211 -34
- package/src/commands/pipeline.ts +186 -0
- package/src/commands/plugin.ts +398 -0
- package/src/commands/profile.ts +342 -0
- package/src/commands/questionnaire.ts +0 -98
- package/src/commands/resume.ts +26 -34
- package/src/commands/rollback.ts +315 -0
- package/src/commands/rollout.ts +88 -0
- package/src/commands/runbook.ts +346 -0
- package/src/commands/schedule.ts +236 -0
- package/src/commands/status.ts +252 -0
- package/src/commands/team-context.ts +220 -0
- package/src/commands/template.ts +58 -57
- package/src/commands/tf/index.ts +70 -11
- package/src/commands/upgrade.ts +57 -0
- package/src/commands/version.ts +54 -50
- package/src/commands/watch.ts +153 -0
- package/src/compat/runtime.ts +1 -1
- package/src/compat/sqlite.ts +75 -5
- package/src/config/mode-store.ts +62 -0
- package/src/config/profiles.ts +84 -0
- package/src/config/types.ts +83 -1
- package/src/config/workspace-state.ts +53 -0
- package/src/engine/cost-estimator.ts +52 -10
- package/src/engine/executor.ts +33 -2
- package/src/engine/planner.ts +68 -1
- package/src/generator/terraform.ts +8 -0
- package/src/history/manager.ts +2 -74
- package/src/hooks/engine.ts +5 -4
- package/src/llm/cost-calculator.ts +2 -2
- package/src/llm/providers/anthropic.ts +50 -21
- package/src/llm/router.ts +76 -7
- package/src/lsp/languages.ts +3 -0
- package/src/lsp/manager.ts +21 -5
- package/src/nimbus.ts +37 -18
- package/src/sessions/manager.ts +108 -1
- package/src/sharing/sync.ts +4 -0
- package/src/sharing/viewer.ts +66 -0
- package/src/tools/file-ops.ts +22 -0
- package/src/tools/schemas/devops.ts +3007 -117
- package/src/tools/schemas/standard.ts +5 -1
- package/src/tools/schemas/types.ts +31 -1
- package/src/tools/spawn-exec.ts +148 -0
- package/src/ui/App.tsx +1183 -66
- package/src/ui/DeployPreview.tsx +62 -57
- package/src/ui/FileDiffModal.tsx +162 -0
- package/src/ui/Header.tsx +87 -24
- package/src/ui/HelpModal.tsx +57 -0
- package/src/ui/InputBox.tsx +163 -10
- package/src/ui/MessageList.tsx +487 -40
- package/src/ui/PermissionPrompt.tsx +17 -5
- package/src/ui/StatusBar.tsx +122 -3
- package/src/ui/TerminalPane.tsx +84 -0
- package/src/ui/ToolCallDisplay.tsx +252 -18
- package/src/ui/TreePane.tsx +132 -0
- package/src/ui/chat-ui.ts +41 -44
- package/src/ui/ink/index.ts +771 -38
- package/src/ui/streaming.ts +1 -1
- package/src/ui/theme.ts +104 -0
- package/src/ui/types.ts +18 -0
- package/src/version.ts +1 -1
- package/src/watcher/index.ts +66 -15
- package/src/wizard/types.ts +1 -0
- package/src/wizard/ui.ts +1 -1
- package/tsconfig.json +2 -2
package/src/ui/ink/index.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
//
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
}
|