@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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming Text Display
|
|
3
|
+
*
|
|
4
|
+
* Utilities for displaying streaming LLM responses in the terminal
|
|
5
|
+
*/
|
|
6
|
+
import { ui } from '../wizard/ui';
|
|
7
|
+
/**
|
|
8
|
+
* StreamingDisplay handles real-time text output from LLM responses
|
|
9
|
+
*/
|
|
10
|
+
export class StreamingDisplay {
|
|
11
|
+
options;
|
|
12
|
+
buffer = '';
|
|
13
|
+
lineStarted = false;
|
|
14
|
+
cursorInterval;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.options = {
|
|
17
|
+
prefix: options.prefix || '',
|
|
18
|
+
prefixColor: options.prefixColor || 'blue',
|
|
19
|
+
showCursor: options.showCursor ?? true,
|
|
20
|
+
cursorChar: options.cursorChar || '|',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Start a new streaming response
|
|
25
|
+
*/
|
|
26
|
+
start() {
|
|
27
|
+
this.buffer = '';
|
|
28
|
+
this.lineStarted = false;
|
|
29
|
+
// Print the prefix
|
|
30
|
+
if (this.options.prefix) {
|
|
31
|
+
ui.write(ui.color(this.options.prefix, this.options.prefixColor));
|
|
32
|
+
}
|
|
33
|
+
this.lineStarted = true;
|
|
34
|
+
// Start cursor blink if enabled
|
|
35
|
+
if (this.options.showCursor) {
|
|
36
|
+
this.startCursor();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Append text to the streaming display
|
|
41
|
+
*/
|
|
42
|
+
append(text) {
|
|
43
|
+
// Stop cursor while writing
|
|
44
|
+
this.stopCursor();
|
|
45
|
+
// Handle newlines in the text
|
|
46
|
+
const lines = text.split('\n');
|
|
47
|
+
for (let i = 0; i < lines.length; i++) {
|
|
48
|
+
const line = lines[i];
|
|
49
|
+
if (i > 0) {
|
|
50
|
+
// This is after a newline
|
|
51
|
+
ui.print(''); // End current line
|
|
52
|
+
this.lineStarted = false;
|
|
53
|
+
}
|
|
54
|
+
if (line) {
|
|
55
|
+
if (!this.lineStarted) {
|
|
56
|
+
// Add indentation for continuation lines
|
|
57
|
+
ui.write(' '); // Indent to align with prefix
|
|
58
|
+
this.lineStarted = true;
|
|
59
|
+
}
|
|
60
|
+
ui.write(line);
|
|
61
|
+
}
|
|
62
|
+
this.buffer += (i > 0 ? '\n' : '') + line;
|
|
63
|
+
}
|
|
64
|
+
// Restart cursor
|
|
65
|
+
if (this.options.showCursor) {
|
|
66
|
+
this.startCursor();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Complete the streaming display
|
|
71
|
+
*/
|
|
72
|
+
complete() {
|
|
73
|
+
this.stopCursor();
|
|
74
|
+
// Ensure we end on a new line
|
|
75
|
+
if (this.lineStarted) {
|
|
76
|
+
ui.print('');
|
|
77
|
+
}
|
|
78
|
+
return this.buffer;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Handle an error during streaming
|
|
82
|
+
*/
|
|
83
|
+
error(message) {
|
|
84
|
+
this.stopCursor();
|
|
85
|
+
if (this.lineStarted) {
|
|
86
|
+
ui.print('');
|
|
87
|
+
}
|
|
88
|
+
ui.error(message);
|
|
89
|
+
}
|
|
90
|
+
startCursor() {
|
|
91
|
+
if (this.cursorInterval) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let visible = true;
|
|
95
|
+
this.cursorInterval = setInterval(() => {
|
|
96
|
+
if (visible) {
|
|
97
|
+
ui.write(ui.dim(this.options.cursorChar));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// Backspace to remove cursor
|
|
101
|
+
ui.write('\b \b');
|
|
102
|
+
}
|
|
103
|
+
visible = !visible;
|
|
104
|
+
}, 500);
|
|
105
|
+
}
|
|
106
|
+
stopCursor() {
|
|
107
|
+
if (this.cursorInterval) {
|
|
108
|
+
clearInterval(this.cursorInterval);
|
|
109
|
+
this.cursorInterval = undefined;
|
|
110
|
+
// Clear any remaining cursor character
|
|
111
|
+
ui.write('\b \b');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Simple streaming helper for one-off usage
|
|
117
|
+
*/
|
|
118
|
+
export async function displayStreaming(generator, options = {}) {
|
|
119
|
+
const display = new StreamingDisplay(options);
|
|
120
|
+
display.start();
|
|
121
|
+
try {
|
|
122
|
+
for await (const chunk of generator) {
|
|
123
|
+
if (chunk.type === 'content' && chunk.content) {
|
|
124
|
+
display.append(chunk.content);
|
|
125
|
+
}
|
|
126
|
+
else if (chunk.type === 'error') {
|
|
127
|
+
display.error(chunk.message || 'Unknown error');
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return display.complete();
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
display.error(error instanceof Error ? error.message : 'Streaming failed');
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme system for Nimbus TUI.
|
|
3
|
+
*
|
|
4
|
+
* Provides named color themes (dark, light) that can be switched at runtime.
|
|
5
|
+
* Components should import `activeTheme` and use its color properties instead
|
|
6
|
+
* of hardcoding color strings.
|
|
7
|
+
*
|
|
8
|
+
* Theme preference is persisted to ~/.nimbus/config.yaml.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import * as os from 'node:os';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Built-in themes
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
export const THEMES = {
|
|
17
|
+
dark: {
|
|
18
|
+
name: 'dark',
|
|
19
|
+
primary: 'cyan',
|
|
20
|
+
secondary: 'blue',
|
|
21
|
+
success: 'green',
|
|
22
|
+
warning: 'yellow',
|
|
23
|
+
error: 'red',
|
|
24
|
+
muted: 'gray',
|
|
25
|
+
border: 'gray',
|
|
26
|
+
},
|
|
27
|
+
light: {
|
|
28
|
+
name: 'light',
|
|
29
|
+
primary: 'blue',
|
|
30
|
+
secondary: 'magenta',
|
|
31
|
+
success: 'green',
|
|
32
|
+
warning: 'yellow',
|
|
33
|
+
error: 'red',
|
|
34
|
+
muted: 'gray',
|
|
35
|
+
border: 'white',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Active theme (mutable singleton)
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
export let activeTheme = THEMES.dark;
|
|
42
|
+
/**
|
|
43
|
+
* Switch the active theme by name.
|
|
44
|
+
* Falls back to `dark` if the name is not recognized.
|
|
45
|
+
* Persists the chosen theme name to ~/.nimbus/config.yaml.
|
|
46
|
+
*/
|
|
47
|
+
export function setTheme(name) {
|
|
48
|
+
activeTheme = THEMES[name] ?? THEMES.dark;
|
|
49
|
+
// Persist to config file
|
|
50
|
+
try {
|
|
51
|
+
const configDir = path.join(os.homedir(), '.nimbus');
|
|
52
|
+
const configFile = path.join(configDir, 'config.yaml');
|
|
53
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
54
|
+
let config = '';
|
|
55
|
+
try {
|
|
56
|
+
config = fs.readFileSync(configFile, 'utf-8');
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* new file */
|
|
60
|
+
}
|
|
61
|
+
if (config.includes('theme:')) {
|
|
62
|
+
config = config.replace(/^theme:.*$/m, `theme: ${name}`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
config = config ? `${config.trim()}\ntheme: ${name}\n` : `theme: ${name}\n`;
|
|
66
|
+
}
|
|
67
|
+
fs.writeFileSync(configFile, config, 'utf-8');
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* ignore FS errors */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* List all available theme names.
|
|
75
|
+
*/
|
|
76
|
+
export function listThemes() {
|
|
77
|
+
return Object.keys(THEMES);
|
|
78
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostHog Analytics Integration
|
|
3
|
+
*
|
|
4
|
+
* Lightweight analytics wrapper that sends events to PostHog.
|
|
5
|
+
* Operates as a complete no-op when POSTHOG_API_KEY is not set.
|
|
6
|
+
*/
|
|
7
|
+
export class Analytics {
|
|
8
|
+
apiKey;
|
|
9
|
+
host;
|
|
10
|
+
constructor() {
|
|
11
|
+
this.apiKey = process.env.POSTHOG_API_KEY;
|
|
12
|
+
this.host = process.env.POSTHOG_HOST || 'https://app.posthog.com';
|
|
13
|
+
}
|
|
14
|
+
get enabled() {
|
|
15
|
+
return !!this.apiKey;
|
|
16
|
+
}
|
|
17
|
+
async trackEvent(event, properties) {
|
|
18
|
+
if (!this.enabled) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
await fetch(`${this.host}/capture/`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
api_key: this.apiKey,
|
|
27
|
+
event,
|
|
28
|
+
properties: {
|
|
29
|
+
...properties,
|
|
30
|
+
timestamp: new Date().toISOString(),
|
|
31
|
+
},
|
|
32
|
+
distinct_id: properties?.userId || 'anonymous',
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Fire-and-forget -- never throw from analytics
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async identifyUser(userId, properties) {
|
|
41
|
+
if (!this.enabled) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await fetch(`${this.host}/capture/`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
api_key: this.apiKey,
|
|
50
|
+
event: '$identify',
|
|
51
|
+
distinct_id: userId,
|
|
52
|
+
properties,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Fire-and-forget
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export const analytics = new Analytics();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost Warning Utility
|
|
3
|
+
*
|
|
4
|
+
* Shows informational cost warnings before destructive operations.
|
|
5
|
+
* Best-effort: silently skips on any failure.
|
|
6
|
+
*/
|
|
7
|
+
import { ui } from '../wizard/ui';
|
|
8
|
+
import { CostEstimator } from '../commands/cost/estimator';
|
|
9
|
+
/**
|
|
10
|
+
* Show an informational cost warning before a destructive operation.
|
|
11
|
+
* Calls CostEstimator.estimateDirectory() and displays the estimated
|
|
12
|
+
* monthly cost impact as a negative value (savings from destroying).
|
|
13
|
+
* Wrapped in try/catch — never throws.
|
|
14
|
+
*/
|
|
15
|
+
export async function showDestructionCostWarning(directory) {
|
|
16
|
+
try {
|
|
17
|
+
const estimate = await CostEstimator.estimateDirectory(directory);
|
|
18
|
+
if (estimate.totalMonthlyCost > 0) {
|
|
19
|
+
ui.warning(`Estimated monthly cost impact: -$${estimate.totalMonthlyCost.toFixed(2)}/month`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Best-effort — silently skip if estimation fails
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ConfigurationError } from './errors';
|
|
2
|
+
export function getEnv(key, defaultValue) {
|
|
3
|
+
const value = process.env[key];
|
|
4
|
+
if (value === undefined) {
|
|
5
|
+
if (defaultValue !== undefined) {
|
|
6
|
+
return defaultValue;
|
|
7
|
+
}
|
|
8
|
+
throw new ConfigurationError(`Environment variable ${key} is required but not set`, 'nimbus', {
|
|
9
|
+
key,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
export function getEnvOptional(key) {
|
|
15
|
+
return process.env[key];
|
|
16
|
+
}
|
|
17
|
+
export function getEnvNumber(key, defaultValue) {
|
|
18
|
+
const value = process.env[key];
|
|
19
|
+
if (value === undefined) {
|
|
20
|
+
if (defaultValue !== undefined) {
|
|
21
|
+
return defaultValue;
|
|
22
|
+
}
|
|
23
|
+
throw new ConfigurationError(`Environment variable ${key} is required but not set`, 'nimbus', {
|
|
24
|
+
key,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const parsed = parseInt(value, 10);
|
|
28
|
+
if (isNaN(parsed)) {
|
|
29
|
+
throw new ConfigurationError(`Environment variable ${key} must be a valid number`, 'nimbus', {
|
|
30
|
+
key,
|
|
31
|
+
value,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
export function getEnvBoolean(key, defaultValue = false) {
|
|
37
|
+
const value = process.env[key];
|
|
38
|
+
if (value === undefined) {
|
|
39
|
+
return defaultValue;
|
|
40
|
+
}
|
|
41
|
+
return value.toLowerCase() === 'true' || value === '1';
|
|
42
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Nimbus Error
|
|
3
|
+
*/
|
|
4
|
+
export class NimbusError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
service;
|
|
7
|
+
timestamp;
|
|
8
|
+
details;
|
|
9
|
+
constructor(message, code, service, details) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'NimbusError';
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.service = service;
|
|
14
|
+
this.timestamp = new Date().toISOString();
|
|
15
|
+
this.details = details;
|
|
16
|
+
}
|
|
17
|
+
toJSON() {
|
|
18
|
+
return {
|
|
19
|
+
code: this.code,
|
|
20
|
+
message: this.message,
|
|
21
|
+
service: this.service,
|
|
22
|
+
timestamp: this.timestamp,
|
|
23
|
+
details: this.details,
|
|
24
|
+
stack: this.stack,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class ValidationError extends NimbusError {
|
|
29
|
+
constructor(message, service, details) {
|
|
30
|
+
super(message, 'VALIDATION_ERROR', service, details);
|
|
31
|
+
this.name = 'ValidationError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class ServiceUnavailableError extends NimbusError {
|
|
35
|
+
constructor(serviceName, details) {
|
|
36
|
+
super(`Service ${serviceName} is unavailable`, 'SERVICE_UNAVAILABLE', 'nimbus', details);
|
|
37
|
+
this.name = 'ServiceUnavailableError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class TimeoutError extends NimbusError {
|
|
41
|
+
constructor(operation, service, timeoutMs) {
|
|
42
|
+
super(`Operation ${operation} timed out after ${timeoutMs}ms`, 'TIMEOUT_ERROR', service, {
|
|
43
|
+
operation,
|
|
44
|
+
timeoutMs,
|
|
45
|
+
});
|
|
46
|
+
this.name = 'TimeoutError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class ConfigurationError extends NimbusError {
|
|
50
|
+
constructor(message, service, details) {
|
|
51
|
+
super(message, 'CONFIGURATION_ERROR', service, details);
|
|
52
|
+
this.name = 'ConfigurationError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
class NimbusEventBus {
|
|
3
|
+
emitter = new EventEmitter();
|
|
4
|
+
constructor() {
|
|
5
|
+
this.emitter.setMaxListeners(50);
|
|
6
|
+
}
|
|
7
|
+
publish(event) {
|
|
8
|
+
this.emitter.emit(event.type, event);
|
|
9
|
+
this.emitter.emit('*', event);
|
|
10
|
+
}
|
|
11
|
+
subscribe(eventType, handler) {
|
|
12
|
+
this.emitter.on(eventType, handler);
|
|
13
|
+
return () => this.emitter.off(eventType, handler);
|
|
14
|
+
}
|
|
15
|
+
once(eventType, handler) {
|
|
16
|
+
this.emitter.once(eventType, handler);
|
|
17
|
+
}
|
|
18
|
+
removeAllListeners(eventType) {
|
|
19
|
+
this.emitter.removeAllListeners(eventType);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const eventBus = new NimbusEventBus();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
export { logger, Logger } from './logger';
|
|
3
|
+
// Errors
|
|
4
|
+
export * from './errors';
|
|
5
|
+
// Validation
|
|
6
|
+
export * from './validation';
|
|
7
|
+
// Environment helpers
|
|
8
|
+
export * from './env';
|
|
9
|
+
// Service authentication
|
|
10
|
+
export * from './service-auth';
|
|
11
|
+
// Rate limiting
|
|
12
|
+
export * from './rate-limiter';
|
|
13
|
+
// Event bus
|
|
14
|
+
export * from './event-bus';
|
|
15
|
+
// Analytics (PostHog)
|
|
16
|
+
export * from './analytics';
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const SENSITIVE_KEYS = new Set([
|
|
2
|
+
'password',
|
|
3
|
+
'passwordhash',
|
|
4
|
+
'password_hash',
|
|
5
|
+
'secret',
|
|
6
|
+
'secretkey',
|
|
7
|
+
'secret_key',
|
|
8
|
+
'clientsecret',
|
|
9
|
+
'client_secret',
|
|
10
|
+
'token',
|
|
11
|
+
'apikey',
|
|
12
|
+
'api_key',
|
|
13
|
+
'api-key',
|
|
14
|
+
'authorization',
|
|
15
|
+
'credentials',
|
|
16
|
+
'credential',
|
|
17
|
+
'accesstoken',
|
|
18
|
+
'access_token',
|
|
19
|
+
'refreshtoken',
|
|
20
|
+
'refresh_token',
|
|
21
|
+
'privatekey',
|
|
22
|
+
'private_key',
|
|
23
|
+
'signingkey',
|
|
24
|
+
'signing_key',
|
|
25
|
+
'encryptionkey',
|
|
26
|
+
'encryption_key',
|
|
27
|
+
'bearer',
|
|
28
|
+
'auth',
|
|
29
|
+
'authtoken',
|
|
30
|
+
'auth_token',
|
|
31
|
+
'sessiontoken',
|
|
32
|
+
'session_token',
|
|
33
|
+
'jwt',
|
|
34
|
+
'jwttoken',
|
|
35
|
+
'jwt_token',
|
|
36
|
+
]);
|
|
37
|
+
const REDACTED = '[REDACTED]';
|
|
38
|
+
function sanitize(value, seen = new WeakSet()) {
|
|
39
|
+
if (value === null || value === undefined) {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === 'bigint') {
|
|
43
|
+
return value.toString();
|
|
44
|
+
}
|
|
45
|
+
if (typeof value !== 'object') {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
const obj = value;
|
|
49
|
+
if (seen.has(obj)) {
|
|
50
|
+
return '[Circular]';
|
|
51
|
+
}
|
|
52
|
+
seen.add(obj);
|
|
53
|
+
if (obj instanceof Date) {
|
|
54
|
+
return obj.toISOString();
|
|
55
|
+
}
|
|
56
|
+
if (obj instanceof Map) {
|
|
57
|
+
return sanitize(Object.fromEntries(obj), seen);
|
|
58
|
+
}
|
|
59
|
+
if (obj instanceof Set) {
|
|
60
|
+
return sanitize([...obj], seen);
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(obj)) {
|
|
63
|
+
return obj.map(item => sanitize(item, seen));
|
|
64
|
+
}
|
|
65
|
+
const sanitized = {};
|
|
66
|
+
for (const key of Object.keys(obj)) {
|
|
67
|
+
if (SENSITIVE_KEYS.has(key.toLowerCase())) {
|
|
68
|
+
sanitized[key] = REDACTED;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
sanitized[key] = sanitize(obj[key], seen);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return sanitized;
|
|
75
|
+
}
|
|
76
|
+
// Patterns to redact in error messages/stacks (connection strings, URLs with creds, etc.)
|
|
77
|
+
const SENSITIVE_PATTERNS = [
|
|
78
|
+
/(?<=:\/\/[^:]+:)[^@]+(?=@)/g, // URL password: protocol://user:PASSWORD@host
|
|
79
|
+
/(?<=password[=:])\s*\S+/gi, // password=VALUE or password: VALUE
|
|
80
|
+
/(?<=secret[=:])\s*\S+/gi, // secret=VALUE or secret: VALUE
|
|
81
|
+
/(?<=token[=:])\s*\S+/gi, // token=VALUE or token: VALUE
|
|
82
|
+
/(?<=apikey[=:])\s*\S+/gi, // apiKey=VALUE or apikey: VALUE
|
|
83
|
+
/(?<=authorization[=:])\s*\S+/gi, // authorization=VALUE
|
|
84
|
+
];
|
|
85
|
+
function sanitizeString(str) {
|
|
86
|
+
let result = str;
|
|
87
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
88
|
+
result = result.replace(pattern, REDACTED);
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
function safeContext(context) {
|
|
93
|
+
if (context instanceof Error) {
|
|
94
|
+
const errorText = context.stack ?? context.message;
|
|
95
|
+
return sanitizeString(errorText);
|
|
96
|
+
}
|
|
97
|
+
return JSON.stringify(sanitize(context));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Simple logger implementation
|
|
101
|
+
*/
|
|
102
|
+
class Logger {
|
|
103
|
+
level;
|
|
104
|
+
levels = ['debug', 'info', 'warn', 'error'];
|
|
105
|
+
constructor(level = 'info') {
|
|
106
|
+
this.level = level;
|
|
107
|
+
}
|
|
108
|
+
shouldLog(level) {
|
|
109
|
+
return this.levels.indexOf(level) >= this.levels.indexOf(this.level);
|
|
110
|
+
}
|
|
111
|
+
debug(message, context) {
|
|
112
|
+
if (this.shouldLog('debug')) {
|
|
113
|
+
const line = this.format('debug', message, context);
|
|
114
|
+
console.log(line);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
info(message, context) {
|
|
118
|
+
if (this.shouldLog('info')) {
|
|
119
|
+
const line = this.format('info', message, context);
|
|
120
|
+
console.log(line);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
warn(message, context) {
|
|
124
|
+
if (this.shouldLog('warn')) {
|
|
125
|
+
const line = this.format('warn', message, context);
|
|
126
|
+
console.warn(line);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
error(message, context) {
|
|
130
|
+
if (this.shouldLog('error')) {
|
|
131
|
+
const line = this.format('error', message, context);
|
|
132
|
+
console.error(line);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
setLevel(level) {
|
|
136
|
+
this.level = level;
|
|
137
|
+
}
|
|
138
|
+
format(level, message, context) {
|
|
139
|
+
const timestamp = new Date().toISOString();
|
|
140
|
+
const levelStr = level.toUpperCase().padEnd(5);
|
|
141
|
+
if (context === undefined) {
|
|
142
|
+
return `[${timestamp}] [${levelStr}] ${message}`;
|
|
143
|
+
}
|
|
144
|
+
return `[${timestamp}] [${levelStr}] ${message} ${safeContext(context)}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Export singleton instance
|
|
148
|
+
export const logger = new Logger(process.env.LOG_LEVEL || 'info');
|
|
149
|
+
// Export Logger class for testing
|
|
150
|
+
export { Logger };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { logger } from './logger';
|
|
2
|
+
export class SimpleRateLimiter {
|
|
3
|
+
requestsPerMinute;
|
|
4
|
+
burstSize;
|
|
5
|
+
buckets;
|
|
6
|
+
cleanupInterval;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.requestsPerMinute = options.requestsPerMinute;
|
|
9
|
+
this.burstSize = options.burstSize || options.requestsPerMinute;
|
|
10
|
+
this.buckets = new Map();
|
|
11
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
|
12
|
+
if (this.cleanupInterval &&
|
|
13
|
+
typeof this.cleanupInterval === 'object' &&
|
|
14
|
+
'unref' in this.cleanupInterval) {
|
|
15
|
+
this.cleanupInterval.unref();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
tryAcquire(clientId = 'default') {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
let bucket = this.buckets.get(clientId);
|
|
21
|
+
if (!bucket) {
|
|
22
|
+
bucket = { tokens: this.burstSize, lastRefill: now };
|
|
23
|
+
this.buckets.set(clientId, bucket);
|
|
24
|
+
}
|
|
25
|
+
const elapsed = now - bucket.lastRefill;
|
|
26
|
+
const refill = (elapsed / 60_000) * this.requestsPerMinute;
|
|
27
|
+
bucket.tokens = Math.min(this.burstSize, bucket.tokens + refill);
|
|
28
|
+
bucket.lastRefill = now;
|
|
29
|
+
if (bucket.tokens >= 1) {
|
|
30
|
+
bucket.tokens -= 1;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
getRemainingRequests(clientId = 'default') {
|
|
36
|
+
const bucket = this.buckets.get(clientId);
|
|
37
|
+
if (!bucket) {
|
|
38
|
+
return this.burstSize;
|
|
39
|
+
}
|
|
40
|
+
const elapsed = Date.now() - bucket.lastRefill;
|
|
41
|
+
const refill = (elapsed / 60_000) * this.requestsPerMinute;
|
|
42
|
+
return Math.min(this.burstSize, Math.floor(bucket.tokens + refill));
|
|
43
|
+
}
|
|
44
|
+
cleanup() {
|
|
45
|
+
const cutoff = Date.now() - 120_000;
|
|
46
|
+
for (const [clientId, bucket] of this.buckets) {
|
|
47
|
+
if (bucket.lastRefill < cutoff) {
|
|
48
|
+
this.buckets.delete(clientId);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
destroy() {
|
|
53
|
+
if (this.cleanupInterval) {
|
|
54
|
+
clearInterval(this.cleanupInterval);
|
|
55
|
+
this.cleanupInterval = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function getClientIp(req) {
|
|
60
|
+
const forwarded = req.headers.get('x-forwarded-for');
|
|
61
|
+
if (forwarded) {
|
|
62
|
+
return forwarded.split(',')[0].trim();
|
|
63
|
+
}
|
|
64
|
+
const realIp = req.headers.get('x-real-ip');
|
|
65
|
+
if (realIp) {
|
|
66
|
+
return realIp;
|
|
67
|
+
}
|
|
68
|
+
return 'unknown';
|
|
69
|
+
}
|
|
70
|
+
export function rateLimitMiddleware(limiter) {
|
|
71
|
+
return (req) => {
|
|
72
|
+
const url = new URL(req.url);
|
|
73
|
+
if (url.pathname === '/health') {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const clientIp = getClientIp(req);
|
|
77
|
+
if (!limiter.tryAcquire(clientIp)) {
|
|
78
|
+
logger.warn(`Rate limited client ${clientIp} on ${url.pathname}`);
|
|
79
|
+
const retryAfter = Math.ceil(60 / limiter.getRemainingRequests(clientIp) || 60);
|
|
80
|
+
return new Response(JSON.stringify({ success: false, error: 'Too Many Requests' }), {
|
|
81
|
+
status: 429,
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
'Retry-After': String(retryAfter),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
}
|