novacode-cli 1.2.0__py3-none-any.whl
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.
- novacode_cli/__init__.py +29 -0
- novacode_cli/__main__.py +6 -0
- novacode_cli/_lazy_heavy.py +99 -0
- novacode_cli/_version.py +3 -0
- novacode_cli/agent_stream.py +178 -0
- novacode_cli/agents/agent_file.py +250 -0
- novacode_cli/agents/agent_mailbox.py +120 -0
- novacode_cli/agents/approval_decisions.py +146 -0
- novacode_cli/agents/arg_repair.py +196 -0
- novacode_cli/agents/async_agents/__init__.py +6 -0
- novacode_cli/agents/async_agents/_config.py +213 -0
- novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
- novacode_cli/agents/async_agents/_specialist.py +84 -0
- novacode_cli/agents/async_agents/_test_commands.py +89 -0
- novacode_cli/agents/async_agents/_tools.py +432 -0
- novacode_cli/agents/async_agents/general_purpose.py +42 -0
- novacode_cli/agents/async_agents/langgraph.json +9 -0
- novacode_cli/agents/async_agents/research.py +22 -0
- novacode_cli/agents/async_context.py +303 -0
- novacode_cli/agents/async_on_demand.py +52 -0
- novacode_cli/agents/async_task_errors.py +75 -0
- novacode_cli/agents/async_workspace.py +27 -0
- novacode_cli/agents/attribution.py +165 -0
- novacode_cli/agents/browser_mode.py +119 -0
- novacode_cli/agents/btw_agent.py +71 -0
- novacode_cli/agents/caveman.py +352 -0
- novacode_cli/agents/caveman_support.py +370 -0
- novacode_cli/agents/code_mode.py +774 -0
- novacode_cli/agents/code_runtime.py +160 -0
- novacode_cli/agents/context_compaction.py +1339 -0
- novacode_cli/agents/core_agent.py +2710 -0
- novacode_cli/agents/default_subagents/__init__.py +24 -0
- novacode_cli/agents/default_subagents/async_subagents.py +296 -0
- novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
- novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
- novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
- novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
- novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
- novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
- novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
- novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
- novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
- novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
- novacode_cli/agents/default_subagents/prompt.py +88 -0
- novacode_cli/agents/default_subagents/subagents.py +138 -0
- novacode_cli/agents/dream_agent.py +93 -0
- novacode_cli/agents/edit_diagnostics.py +362 -0
- novacode_cli/agents/edit_fallback.py +62 -0
- novacode_cli/agents/handoff.py +166 -0
- novacode_cli/agents/invocation.py +152 -0
- novacode_cli/agents/model_budget.py +42 -0
- novacode_cli/agents/model_retry.py +113 -0
- novacode_cli/agents/model_router.py +420 -0
- novacode_cli/agents/openai_decisions.py +186 -0
- novacode_cli/agents/plan_agent/__init__.py +12 -0
- novacode_cli/agents/plan_agent/plan_agent.py +357 -0
- novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
- novacode_cli/agents/plan_execution.py +237 -0
- novacode_cli/agents/ponytail.py +66 -0
- novacode_cli/agents/prime.py +274 -0
- novacode_cli/agents/provider_caching.py +99 -0
- novacode_cli/agents/server_launcher.py +904 -0
- novacode_cli/agents/strict_tools.py +384 -0
- novacode_cli/agents/task_discipline.py +294 -0
- novacode_cli/agents/tool_call_repair.py +290 -0
- novacode_cli/agents/tool_offload.py +291 -0
- novacode_cli/agents/tool_search.py +806 -0
- novacode_cli/agents/tool_search_variants.py +106 -0
- novacode_cli/agents/tool_verdicts.py +1194 -0
- novacode_cli/agents/user_async_agents.py +312 -0
- novacode_cli/artifacts/__init__.py +10 -0
- novacode_cli/artifacts/registry.py +278 -0
- novacode_cli/artifacts/server.py +426 -0
- novacode_cli/assets/Nova.png +0 -0
- novacode_cli/assets/nova-avatar.png +0 -0
- novacode_cli/audio/__init__.py +301 -0
- novacode_cli/audio/capture.py +137 -0
- novacode_cli/audio/downloads.py +273 -0
- novacode_cli/audio/pipeline.py +391 -0
- novacode_cli/audio/playback.py +87 -0
- novacode_cli/audio/providers.py +208 -0
- novacode_cli/audio/speakable.py +78 -0
- novacode_cli/audio/stt.py +132 -0
- novacode_cli/audio/stt_deepgram.py +69 -0
- novacode_cli/audio/stt_elevenlabs.py +89 -0
- novacode_cli/audio/stt_parakeet.py +94 -0
- novacode_cli/audio/stt_whistle.py +61 -0
- novacode_cli/audio/summarize.py +92 -0
- novacode_cli/audio/tts.py +85 -0
- novacode_cli/audio/tts_kokoro.py +73 -0
- novacode_cli/audio/tts_orpheus.py +180 -0
- novacode_cli/audio/tts_pocket.py +131 -0
- novacode_cli/audio/tts_sano.py +69 -0
- novacode_cli/audio/vad.py +145 -0
- novacode_cli/backends/__init__.py +17 -0
- novacode_cli/backends/filesystem.py +894 -0
- novacode_cli/backends/native_search.py +92 -0
- novacode_cli/bootstrap/__init__.py +9 -0
- novacode_cli/bootstrap/env_snapshot.py +240 -0
- novacode_cli/bootstrap/steering.py +464 -0
- novacode_cli/bootstrap/vision_router.py +604 -0
- novacode_cli/brand.py +362 -0
- novacode_cli/cli_args.py +718 -0
- novacode_cli/commands/__init__.py +237 -0
- novacode_cli/commands/agents_commands.py +231 -0
- novacode_cli/commands/auth_handler.py +40 -0
- novacode_cli/commands/browser_use_handler.py +75 -0
- novacode_cli/commands/cache_handler.py +32 -0
- novacode_cli/commands/chat_handler.py +1357 -0
- novacode_cli/commands/ci_fix.py +171 -0
- novacode_cli/commands/claude_plan.py +331 -0
- novacode_cli/commands/codex_plan.py +195 -0
- novacode_cli/commands/commands.py +1113 -0
- novacode_cli/commands/create_handler.py +124 -0
- novacode_cli/commands/create_server.py +1887 -0
- novacode_cli/commands/cron_handler.py +119 -0
- novacode_cli/commands/dream_handler.py +203 -0
- novacode_cli/commands/effort_handler.py +65 -0
- novacode_cli/commands/evolution_handler.py +214 -0
- novacode_cli/commands/file_commands.py +381 -0
- novacode_cli/commands/handoff_handler.py +68 -0
- novacode_cli/commands/hooks_handler.py +269 -0
- novacode_cli/commands/init_handler.py +146 -0
- novacode_cli/commands/learning_handler.py +160 -0
- novacode_cli/commands/lite_mode_handler.py +76 -0
- novacode_cli/commands/log_commands.py +363 -0
- novacode_cli/commands/mcp_handler.py +29 -0
- novacode_cli/commands/model_handler.py +29 -0
- novacode_cli/commands/notifications_handler.py +158 -0
- novacode_cli/commands/onboard_handler.py +121 -0
- novacode_cli/commands/plan_handler.py +440 -0
- novacode_cli/commands/planner_usage.py +250 -0
- novacode_cli/commands/plugin_install_handler.py +38 -0
- novacode_cli/commands/plugins_handler.py +35 -0
- novacode_cli/commands/prime_handler.py +455 -0
- novacode_cli/commands/prompt_handler.py +149 -0
- novacode_cli/commands/provider_usage.py +160 -0
- novacode_cli/commands/ralph_events.py +106 -0
- novacode_cli/commands/ralph_handler.py +1293 -0
- novacode_cli/commands/refine_handler.py +213 -0
- novacode_cli/commands/research_handler.py +202 -0
- novacode_cli/commands/rootapp_remote.py +83 -0
- novacode_cli/commands/router_handler.py +36 -0
- novacode_cli/commands/server_commands.py +249 -0
- novacode_cli/commands/session_commands.py +203 -0
- novacode_cli/commands/share_handler.py +450 -0
- novacode_cli/commands/side_commands.py +366 -0
- novacode_cli/commands/skill_invoke.py +420 -0
- novacode_cli/commands/skills_commands.py +48 -0
- novacode_cli/commands/trace_handler.py +322 -0
- novacode_cli/commands/trello_handler.py +226 -0
- novacode_cli/commands/trello_remote.py +177 -0
- novacode_cli/commands/trello_server.py +1214 -0
- novacode_cli/commands/vision_handler.py +127 -0
- novacode_cli/commands/voice_handler.py +447 -0
- novacode_cli/commands/webhook_handler.py +134 -0
- novacode_cli/commands/wiki_commands.py +227 -0
- novacode_cli/compaction.py +753 -0
- novacode_cli/computation_cache.py +159 -0
- novacode_cli/computer/__init__.py +1 -0
- novacode_cli/computer/persistent.py +234 -0
- novacode_cli/computer/preview.py +253 -0
- novacode_cli/computer/runtime.py +474 -0
- novacode_cli/computer/windows_console.py +152 -0
- novacode_cli/computer/windows_qemu.py +211 -0
- novacode_cli/config/browser_mode.py +159 -0
- novacode_cli/config/catalog_models.py +95 -0
- novacode_cli/config/config.py +1388 -0
- novacode_cli/config/credentials.py +540 -0
- novacode_cli/config/google_oauth_auth.py +281 -0
- novacode_cli/config/model_capabilities.py +343 -0
- novacode_cli/config/model_catalog.py +275 -0
- novacode_cli/config/model_create.py +1325 -0
- novacode_cli/config/model_manager.py +695 -0
- novacode_cli/config/nova_config.py +1283 -0
- novacode_cli/config/openai_chatgpt_auth.py +384 -0
- novacode_cli/config/opencode_gateway.py +25 -0
- novacode_cli/config/plan_mode.py +188 -0
- novacode_cli/config/provider_auth.py +438 -0
- novacode_cli/config/provider_catalog.json +5127 -0
- novacode_cli/config/provider_registry.py +38 -0
- novacode_cli/config/provider_transports.py +258 -0
- novacode_cli/config/role_models.py +175 -0
- novacode_cli/context/__init__.py +50 -0
- novacode_cli/context/_analysis.py +799 -0
- novacode_cli/context/_budget.py +116 -0
- novacode_cli/context/_conversation.py +73 -0
- novacode_cli/context/_dynamic.py +264 -0
- novacode_cli/context/_model_config.py +380 -0
- novacode_cli/context/_models_dev.py +99 -0
- novacode_cli/context/archives.py +248 -0
- novacode_cli/context/estimates.py +65 -0
- novacode_cli/context/history.py +17 -0
- novacode_cli/context/knowledge_cache.py +292 -0
- novacode_cli/context/manager.py +101 -0
- novacode_cli/context/observations.py +65 -0
- novacode_cli/context/pressure.py +168 -0
- novacode_cli/context/provider_accounting.py +109 -0
- novacode_cli/context/snapshot.py +493 -0
- novacode_cli/context/strategies.py +279 -0
- novacode_cli/context/task_progress.py +122 -0
- novacode_cli/core/__init__.py +0 -0
- novacode_cli/core/agent_loop.py +1821 -0
- novacode_cli/core/autonomous_loop.py +277 -0
- novacode_cli/core/code_approvals.py +57 -0
- novacode_cli/core/input_preparation.py +276 -0
- novacode_cli/core/reasoning.py +67 -0
- novacode_cli/core/streaming.py +326 -0
- novacode_cli/core/subagent_tasks.py +696 -0
- novacode_cli/core/subagent_tracking.py +235 -0
- novacode_cli/core/tool_results.py +116 -0
- novacode_cli/core/verification_loop.py +272 -0
- novacode_cli/council.py +595 -0
- novacode_cli/council_consensus.py +115 -0
- novacode_cli/council_decision.py +157 -0
- novacode_cli/council_planner.py +1421 -0
- novacode_cli/council_settings.py +208 -0
- novacode_cli/council_web_settings.py +41 -0
- novacode_cli/council_workspace.py +56 -0
- novacode_cli/cowork/__init__.py +10 -0
- novacode_cli/cowork/broker_middleware.py +162 -0
- novacode_cli/cowork/launcher.py +102 -0
- novacode_cli/cowork/policy.py +284 -0
- novacode_cli/cowork/ui.py +826 -0
- novacode_cli/daemons/__init__.py +1 -0
- novacode_cli/daemons/registry.py +342 -0
- novacode_cli/desktop_notifications.py +302 -0
- novacode_cli/doctor.py +251 -0
- novacode_cli/embeddings.py +109 -0
- novacode_cli/entrypoint.py +56 -0
- novacode_cli/errors/__init__.py +19 -0
- novacode_cli/errors/handlers.py +648 -0
- novacode_cli/errors/provider_errors.py +249 -0
- novacode_cli/errors/taxonomy.py +80 -0
- novacode_cli/events.py +179 -0
- novacode_cli/file_ops.py +714 -0
- novacode_cli/git_safety.py +281 -0
- novacode_cli/handoff.py +383 -0
- novacode_cli/headless/__init__.py +18 -0
- novacode_cli/headless/input.py +58 -0
- novacode_cli/headless/output.py +224 -0
- novacode_cli/headless/pipe.py +532 -0
- novacode_cli/headless/runner.py +461 -0
- novacode_cli/hermes/__init__.py +39 -0
- novacode_cli/hermes/activation.py +297 -0
- novacode_cli/hermes/config.py +129 -0
- novacode_cli/hermes/curator.py +199 -0
- novacode_cli/hermes/evolution.py +374 -0
- novacode_cli/hermes/ledger.py +364 -0
- novacode_cli/hermes/memory_tiers.py +969 -0
- novacode_cli/hermes/middleware.py +678 -0
- novacode_cli/hermes/overview.py +174 -0
- novacode_cli/hermes/prompt_evolution.py +503 -0
- novacode_cli/hermes/refine_loop.py +643 -0
- novacode_cli/hermes/refinement_log.py +190 -0
- novacode_cli/hermes/review.py +674 -0
- novacode_cli/hermes/skill_debate.py +135 -0
- novacode_cli/hermes/skill_discovery.py +450 -0
- novacode_cli/hermes/skill_manager.py +335 -0
- novacode_cli/hermes/skill_proposals.py +43 -0
- novacode_cli/hermes/test_gaming.py +119 -0
- novacode_cli/hermes/tracker.py +435 -0
- novacode_cli/hermes/tuner.py +206 -0
- novacode_cli/hermes/verification_memory.py +134 -0
- novacode_cli/hermes/verifier.py +277 -0
- novacode_cli/hitl/__init__.py +22 -0
- novacode_cli/hitl/interrupts.py +493 -0
- novacode_cli/hooks.py +359 -0
- novacode_cli/image_utils.py +517 -0
- novacode_cli/init/__init__.py +7 -0
- novacode_cli/init/events.py +43 -0
- novacode_cli/input_utils.py +526 -0
- novacode_cli/install_commands.py +87 -0
- novacode_cli/integrations/__init__.py +82 -0
- novacode_cli/integrations/daytona.py +122 -0
- novacode_cli/integrations/docker.py +309 -0
- novacode_cli/integrations/host_path.py +111 -0
- novacode_cli/integrations/langsmith.py +151 -0
- novacode_cli/integrations/modal.py +134 -0
- novacode_cli/integrations/runloop.py +130 -0
- novacode_cli/integrations/sandbox_factory.py +1388 -0
- novacode_cli/integrations/sandbox_registry.py +312 -0
- novacode_cli/integrations/workdir_backend.py +589 -0
- novacode_cli/main.py +2078 -0
- novacode_cli/mcp/__init__.py +73 -0
- novacode_cli/mcp/auth.py +189 -0
- novacode_cli/mcp/client.py +571 -0
- novacode_cli/mcp/code_mode.py +5 -0
- novacode_cli/mcp/code_runtime.py +5 -0
- novacode_cli/mcp/commands.py +902 -0
- novacode_cli/mcp/config.py +406 -0
- novacode_cli/mcp/generic.py +128 -0
- novacode_cli/mcp/middleware.py +993 -0
- novacode_cli/mcp/presets.py +367 -0
- novacode_cli/memory/__init__.py +1 -0
- novacode_cli/memory/agent_memory.py +1312 -0
- novacode_cli/memory/builtin_lessons.py +60 -0
- novacode_cli/memory/dream_gates.py +192 -0
- novacode_cli/memory/limits.py +59 -0
- novacode_cli/memory/parsed_cache.py +106 -0
- novacode_cli/memory/recall_stats.py +97 -0
- novacode_cli/memory/semantic.py +116 -0
- novacode_cli/memory/store.py +335 -0
- novacode_cli/memory/wiki.py +274 -0
- novacode_cli/migrate.py +185 -0
- novacode_cli/onboarding.py +649 -0
- novacode_cli/onboarding_check.py +119 -0
- novacode_cli/path_approval.py +297 -0
- novacode_cli/plan_archive.py +148 -0
- novacode_cli/plugins/__init__.py +121 -0
- novacode_cli/plugins/claude_plugins.py +538 -0
- novacode_cli/plugins/cli.py +168 -0
- novacode_cli/plugins/components.py +413 -0
- novacode_cli/plugins/hooks.py +356 -0
- novacode_cli/plugins/loader.py +317 -0
- novacode_cli/plugins/marketplaces.py +199 -0
- novacode_cli/plugins/storage.py +70 -0
- novacode_cli/prime.py +759 -0
- novacode_cli/prime_factory/__init__.py +15 -0
- novacode_cli/prime_factory/container.py +198 -0
- novacode_cli/prime_factory/controller.py +565 -0
- novacode_cli/prime_factory/knowledge.py +261 -0
- novacode_cli/prime_factory/publication.py +218 -0
- novacode_cli/prime_factory/recovery.py +50 -0
- novacode_cli/prime_factory/research.py +370 -0
- novacode_cli/prime_factory/runner.py +608 -0
- novacode_cli/prime_factory/runtime.py +261 -0
- novacode_cli/prime_factory/state.py +201 -0
- novacode_cli/prime_factory/store.py +128 -0
- novacode_cli/prime_factory/worker.py +129 -0
- novacode_cli/prime_factory/workflow.py +899 -0
- novacode_cli/prime_factory/workspace.py +317 -0
- novacode_cli/prime_memory.py +290 -0
- novacode_cli/process_manager.py +585 -0
- novacode_cli/project_onboard.py +537 -0
- novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
- novacode_cli/prompts/__init__.py +168 -0
- novacode_cli/prompts/agent_definitions.jinja +11 -0
- novacode_cli/prompts/agent_generation.jinja +125 -0
- novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
- novacode_cli/prompts/claude_plan_mode.jinja +12 -0
- novacode_cli/prompts/coding_workflow.jinja +18 -0
- novacode_cli/prompts/computer_use.jinja +17 -0
- novacode_cli/prompts/core_agent_system.jinja +81 -0
- novacode_cli/prompts/delegation.jinja +15 -0
- novacode_cli/prompts/desktop_power.jinja +4 -0
- novacode_cli/prompts/file_paths.jinja +15 -0
- novacode_cli/prompts/file_tracker.jinja +23 -0
- novacode_cli/prompts/init_agents_md.jinja +56 -0
- novacode_cli/prompts/init_exploration.jinja +174 -0
- novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
- novacode_cli/prompts/instruction_scope.jinja +5 -0
- novacode_cli/prompts/learnings_extraction.jinja +45 -0
- novacode_cli/prompts/longterm_memory.jinja +54 -0
- novacode_cli/prompts/mcp.jinja +84 -0
- novacode_cli/prompts/memory_consolidation.jinja +40 -0
- novacode_cli/prompts/nova_evolution.jinja +73 -0
- novacode_cli/prompts/nova_review.jinja +158 -0
- novacode_cli/prompts/nova_skill_debate.jinja +36 -0
- novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
- novacode_cli/prompts/nova_verify.jinja +100 -0
- novacode_cli/prompts/onboard_dashboard.jinja +378 -0
- novacode_cli/prompts/plan_agent.jinja +87 -0
- novacode_cli/prompts/plan_scouts.jinja +5 -0
- novacode_cli/prompts/plan_storage.jinja +8 -0
- novacode_cli/prompts/planning.jinja +82 -0
- novacode_cli/prompts/ponytail/LICENSE +21 -0
- novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
- novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
- novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
- novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
- novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
- novacode_cli/prompts/prompt_evolution.jinja +27 -0
- novacode_cli/prompts/ralph_iteration.jinja +75 -0
- novacode_cli/prompts/refine_gate.jinja +31 -0
- novacode_cli/prompts/refine_plan.jinja +64 -0
- novacode_cli/prompts/refine_review.jinja +34 -0
- novacode_cli/prompts/research_delivery.jinja +9 -0
- novacode_cli/prompts/research_swarm.jinja +130 -0
- novacode_cli/prompts/response_style.jinja +5 -0
- novacode_cli/prompts/session_summarization.jinja +86 -0
- novacode_cli/prompts/skill_creation.jinja +85 -0
- novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
- novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
- novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
- novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
- novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
- novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
- novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
- novacode_cli/prompts/summarization.jinja +107 -0
- novacode_cli/prompts/summarization_answers.jinja +23 -0
- novacode_cli/prompts/summarization_questions.jinja +32 -0
- novacode_cli/prompts/vision_caption.jinja +21 -0
- novacode_cli/prompts/voice_summary.jinja +65 -0
- novacode_cli/prompts/wiki_file.jinja +57 -0
- novacode_cli/prompts/wiki_ingest.jinja +87 -0
- novacode_cli/recovery.py +383 -0
- novacode_cli/remote/__init__.py +45 -0
- novacode_cli/remote/approvals.py +282 -0
- novacode_cli/remote/async_task_clients.py +97 -0
- novacode_cli/remote/async_task_watcher.py +240 -0
- novacode_cli/remote/bridge.py +905 -0
- novacode_cli/remote/command_menu.py +26 -0
- novacode_cli/remote/command_ui.py +243 -0
- novacode_cli/remote/config.py +199 -0
- novacode_cli/remote/discord_bridge.py +530 -0
- novacode_cli/remote/discord_format.py +31 -0
- novacode_cli/remote/discord_threads.py +225 -0
- novacode_cli/remote/images.py +97 -0
- novacode_cli/remote/processor.py +601 -0
- novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
- novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
- novacode_cli/remote/rootapp_bot/package.json +18 -0
- novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
- novacode_cli/remote/rootapp_bridge.py +370 -0
- novacode_cli/remote/rootapp_setup.py +89 -0
- novacode_cli/remote/routing.py +103 -0
- novacode_cli/remote/scheduler.py +302 -0
- novacode_cli/remote/session.py +164 -0
- novacode_cli/remote/status.py +411 -0
- novacode_cli/remote/streaming.py +188 -0
- novacode_cli/remote/telegram_bridge.py +1118 -0
- novacode_cli/remote/telegram_format.py +215 -0
- novacode_cli/remote/telegram_hub.py +256 -0
- novacode_cli/remote/voice_notes.py +120 -0
- novacode_cli/remote/webhook_adapters.py +167 -0
- novacode_cli/remote/webhook_server.py +154 -0
- novacode_cli/rewind.py +150 -0
- novacode_cli/rooms/__init__.py +1 -0
- novacode_cli/rooms/bridge.py +86 -0
- novacode_cli/rooms/cli.py +231 -0
- novacode_cli/rooms/nova.py +227 -0
- novacode_cli/rooms/runtime.py +299 -0
- novacode_cli/rooms/store.py +538 -0
- novacode_cli/rooms/workspace.py +94 -0
- novacode_cli/security/__init__.py +29 -0
- novacode_cli/security/approval_context.py +45 -0
- novacode_cli/security/delegated_approval.py +68 -0
- novacode_cli/security/local_http.py +85 -0
- novacode_cli/security/middleware.py +87 -0
- novacode_cli/security/paths.py +35 -0
- novacode_cli/security/policy.py +535 -0
- novacode_cli/security/policy_writer.py +90 -0
- novacode_cli/security/prompt_injection.py +203 -0
- novacode_cli/security/remember.py +81 -0
- novacode_cli/security/rule_synthesis.py +116 -0
- novacode_cli/security/secret_files.py +61 -0
- novacode_cli/security/session_allow.py +114 -0
- novacode_cli/security/unicode_security.py +516 -0
- novacode_cli/security/validator.py +173 -0
- novacode_cli/server/__init__.py +9 -0
- novacode_cli/server/app.py +632 -0
- novacode_cli/server/event_adapter.py +189 -0
- novacode_cli/server/session_manager.py +90 -0
- novacode_cli/server_main.py +64 -0
- novacode_cli/server_runner/dev_server.py +530 -0
- novacode_cli/server_runner/test_runner.py +470 -0
- novacode_cli/session/__init__.py +1 -0
- novacode_cli/session/adapters/__init__.py +240 -0
- novacode_cli/session/adapters/claude_code.py +102 -0
- novacode_cli/session/adapters/codex.py +77 -0
- novacode_cli/session/adapters/harness_native.py +75 -0
- novacode_cli/session/checkpoint_coordinator.py +448 -0
- novacode_cli/session/context_restore.py +25 -0
- novacode_cli/session/history_export.py +228 -0
- novacode_cli/session/history_tree.py +174 -0
- novacode_cli/session/imported_context.py +267 -0
- novacode_cli/session/runtime_identity.py +32 -0
- novacode_cli/session/session_persistence.py +1168 -0
- novacode_cli/session/session_prompt_builder.py +279 -0
- novacode_cli/session/session_restore.py +351 -0
- novacode_cli/session/session_summarization.py +20 -0
- novacode_cli/session/transcript_journal.py +153 -0
- novacode_cli/sessions/__init__.py +11 -0
- novacode_cli/sessions/gateway.py +84 -0
- novacode_cli/sessions/launch.py +51 -0
- novacode_cli/sessions/lease.py +213 -0
- novacode_cli/sessions/messaging.py +255 -0
- novacode_cli/sessions/protocol.py +216 -0
- novacode_cli/sessions/resume.py +31 -0
- novacode_cli/sessions/supervisor.py +498 -0
- novacode_cli/sessions/tasks.py +73 -0
- novacode_cli/sessions/tui_messaging.py +413 -0
- novacode_cli/sessions/worker.py +652 -0
- novacode_cli/sessions/worktree.py +262 -0
- novacode_cli/shell/__init__.py +27 -0
- novacode_cli/shell/jobs.py +475 -0
- novacode_cli/shell/middleware.py +1568 -0
- novacode_cli/shell/os_sandbox.py +205 -0
- novacode_cli/shell/output.py +181 -0
- novacode_cli/shell/patterns.py +275 -0
- novacode_cli/shell/processes.py +87 -0
- novacode_cli/shell/utils.py +236 -0
- novacode_cli/skills/__init__.py +26 -0
- novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
- novacode_cli/skills/curation_middleware.py +69 -0
- novacode_cli/skills/executable.py +161 -0
- novacode_cli/skills/libraries.py +102 -0
- novacode_cli/skills/load.py +248 -0
- novacode_cli/skills/prune.py +215 -0
- novacode_cli/skills/refreshing_middleware.py +896 -0
- novacode_cli/skills/retrieval.py +248 -0
- novacode_cli/skills/runtime.py +118 -0
- novacode_cli/skills/schema.py +254 -0
- novacode_cli/skills/skill_creation.py +1549 -0
- novacode_cli/skills/skill_lock.py +75 -0
- novacode_cli/skills/skill_system_prompt.py +16 -0
- novacode_cli/skills/skills_prefs.py +126 -0
- novacode_cli/skills/tool_resolver.py +31 -0
- novacode_cli/skills/upstream.py +50 -0
- novacode_cli/skills/versioning.py +291 -0
- novacode_cli/states/Session.py +847 -0
- novacode_cli/states/__init__.py +31 -0
- novacode_cli/states/slices/__init__.py +17 -0
- novacode_cli/states/slices/agent_runtime.py +249 -0
- novacode_cli/states/slices/background_tasks.py +21 -0
- novacode_cli/states/slices/notifications.py +74 -0
- novacode_cli/states/slices/remote_bridge.py +56 -0
- novacode_cli/states/slices/ui_settings.py +49 -0
- novacode_cli/states/slices/wiki.py +25 -0
- novacode_cli/teammates/__init__.py +1 -0
- novacode_cli/teammates/controller.py +557 -0
- novacode_cli/teammates/graph.py +104 -0
- novacode_cli/teammates/registry.py +175 -0
- novacode_cli/teammates/service.py +254 -0
- novacode_cli/teammates/worker.py +19 -0
- novacode_cli/teams/__init__.py +1 -0
- novacode_cli/teams/adapters/__init__.py +1 -0
- novacode_cli/teams/adapters/container.py +316 -0
- novacode_cli/teams/adapters/inprocess.py +206 -0
- novacode_cli/teams/adapters/process.py +364 -0
- novacode_cli/teams/bootstrap.py +246 -0
- novacode_cli/teams/container_git.py +67 -0
- novacode_cli/teams/context.py +34 -0
- novacode_cli/teams/coordinator.py +815 -0
- novacode_cli/teams/gateway.py +200 -0
- novacode_cli/teams/host.py +64 -0
- novacode_cli/teams/legacy.py +52 -0
- novacode_cli/teams/middleware.py +176 -0
- novacode_cli/teams/models.py +104 -0
- novacode_cli/teams/output.py +47 -0
- novacode_cli/teams/protocol.py +50 -0
- novacode_cli/teams/recovery.py +80 -0
- novacode_cli/teams/scheduler.py +44 -0
- novacode_cli/teams/store.py +631 -0
- novacode_cli/teams/worker.py +251 -0
- novacode_cli/teams/workspace.py +100 -0
- novacode_cli/token_utils.py +256 -0
- novacode_cli/tools/__init__.py +104 -0
- novacode_cli/tools/_shared.py +97 -0
- novacode_cli/tools/_types.py +24 -0
- novacode_cli/tools/artifact_tools.py +137 -0
- novacode_cli/tools/code_search_tools.py +330 -0
- novacode_cli/tools/computer_tools.py +77 -0
- novacode_cli/tools/cron_tools.py +65 -0
- novacode_cli/tools/daemon_tool.py +290 -0
- novacode_cli/tools/desktop_tools.py +161 -0
- novacode_cli/tools/fetch_tools.py +530 -0
- novacode_cli/tools/http_tools.py +279 -0
- novacode_cli/tools/inspection_tools.py +107 -0
- novacode_cli/tools/job_tools.py +143 -0
- novacode_cli/tools/knowledge_tools.py +427 -0
- novacode_cli/tools/memory_tools.py +461 -0
- novacode_cli/tools/memory_wiki_tools.py +40 -0
- novacode_cli/tools/notebook_tools.py +238 -0
- novacode_cli/tools/oracle_tool.py +83 -0
- novacode_cli/tools/plan_archive_tools.py +80 -0
- novacode_cli/tools/plan_mode_tools.py +315 -0
- novacode_cli/tools/python_kernel_loop.py +165 -0
- novacode_cli/tools/python_kernel_tool.py +222 -0
- novacode_cli/tools/reflection_tools.py +34 -0
- novacode_cli/tools/scraper_tools.py +377 -0
- novacode_cli/tools/skill_tools.py +275 -0
- novacode_cli/tools/speak_tool.py +51 -0
- novacode_cli/tools/store_memory_tools.py +227 -0
- novacode_cli/tools/ui_tools.py +62 -0
- novacode_cli/tools/waiter_tools.py +408 -0
- novacode_cli/tools/web_tools.py +349 -0
- novacode_cli/tools/wiki_tools.py +152 -0
- novacode_cli/tools/worktree_tools.py +219 -0
- novacode_cli/tracking/__init__.py +10 -0
- novacode_cli/tracking/file_tracker.py +826 -0
- novacode_cli/tracking/local_metrics.py +170 -0
- novacode_cli/tracking/loop_guard.py +507 -0
- novacode_cli/tracking/request_metrics.py +60 -0
- novacode_cli/tracking/tracing.py +692 -0
- novacode_cli/tracking/usage_tree.py +677 -0
- novacode_cli/tracking/workspace_anchoring.py +231 -0
- novacode_cli/tui/__init__.py +11 -0
- novacode_cli/tui/animation_rate.py +9 -0
- novacode_cli/tui/animations.py +332 -0
- novacode_cli/tui/app.py +19315 -0
- novacode_cli/tui/auth_display.py +87 -0
- novacode_cli/tui/auth_screens.py +826 -0
- novacode_cli/tui/avatar.py +294 -0
- novacode_cli/tui/avatar_frames.py +21 -0
- novacode_cli/tui/background_tasks.py +35 -0
- novacode_cli/tui/browser_settings_screen.py +70 -0
- novacode_cli/tui/caveman_screen.py +185 -0
- novacode_cli/tui/council_settings_screen.py +182 -0
- novacode_cli/tui/council_view.py +535 -0
- novacode_cli/tui/decision_settings.py +268 -0
- novacode_cli/tui/diff_preview.py +199 -0
- novacode_cli/tui/gc_tuning.py +63 -0
- novacode_cli/tui/handoff_screen.py +206 -0
- novacode_cli/tui/harness.py +244 -0
- novacode_cli/tui/history_tree_screen.py +179 -0
- novacode_cli/tui/home.py +328 -0
- novacode_cli/tui/home_scene.py +94 -0
- novacode_cli/tui/icons.py +81 -0
- novacode_cli/tui/interface_styles.tcss +327 -0
- novacode_cli/tui/matrix3.py +108 -0
- novacode_cli/tui/mcp_tools_screen.py +131 -0
- novacode_cli/tui/motion.py +178 -0
- novacode_cli/tui/output_body.py +136 -0
- novacode_cli/tui/output_buffer.py +59 -0
- novacode_cli/tui/palette.py +334 -0
- novacode_cli/tui/pickers.py +237 -0
- novacode_cli/tui/prime_panel.py +506 -0
- novacode_cli/tui/provider_signin.py +213 -0
- novacode_cli/tui/pty_shell.py +159 -0
- novacode_cli/tui/reference_screens.py +444 -0
- novacode_cli/tui/resume_picker.py +346 -0
- novacode_cli/tui/rooms.py +597 -0
- novacode_cli/tui/screens.py +7088 -0
- novacode_cli/tui/session_import.py +333 -0
- novacode_cli/tui/session_pane.py +242 -0
- novacode_cli/tui/session_tab.py +42 -0
- novacode_cli/tui/session_transcript.py +118 -0
- novacode_cli/tui/shell_output.py +7 -0
- novacode_cli/tui/stall_watch.py +174 -0
- novacode_cli/tui/statusline.py +40 -0
- novacode_cli/tui/statusline_screen.py +91 -0
- novacode_cli/tui/teammate_updates.py +162 -0
- novacode_cli/tui/teammates.py +548 -0
- novacode_cli/tui/terminal.py +289 -0
- novacode_cli/tui/terminal_view.py +235 -0
- novacode_cli/tui/themes.py +118 -0
- novacode_cli/tui/update_notice.py +84 -0
- novacode_cli/tui/voice_downloads.py +113 -0
- novacode_cli/tui/widgets.py +2038 -0
- novacode_cli/tui/windows_input.py +38 -0
- novacode_cli/tui/workspace_approval.py +165 -0
- novacode_cli/ui/council_web.py +113 -0
- novacode_cli/ui/council_workspace.py +72 -0
- novacode_cli/ui/cowork_web.py +41 -0
- novacode_cli/ui/diff_highlight.py +160 -0
- novacode_cli/ui/execution.py +1396 -0
- novacode_cli/ui/exit_summary.py +65 -0
- novacode_cli/ui/hitl_approval.py +196 -0
- novacode_cli/ui/input_preparation.py +2 -0
- novacode_cli/ui/interrupt_handlers.py +215 -0
- novacode_cli/ui/question_prompt.py +151 -0
- novacode_cli/ui/status_phrases.py +157 -0
- novacode_cli/ui/stream_preview.py +66 -0
- novacode_cli/ui/streaming.py +2 -0
- novacode_cli/ui/subagent_tracking.py +2 -0
- novacode_cli/ui/ui_elements.py +1585 -0
- novacode_cli/ui/web_workspace.py +244 -0
- novacode_cli/ui_events.py +299 -0
- novacode_cli/update_ui.py +243 -0
- novacode_cli/updates.py +489 -0
- novacode_cli/utils/backend_patches.py +399 -0
- novacode_cli/utils/backend_paths.py +467 -0
- novacode_cli/utils/lazy_providers.py +51 -0
- novacode_cli/utils/model_info.py +223 -0
- novacode_cli/utils/pdf_extraction.py +183 -0
- novacode_cli/utils/startup.py +29 -0
- novacode_cli/utils/tasks.py +42 -0
- novacode_cli/utils/windows.py +9 -0
- novacode_cli/vixie/hook_adapter.py +127 -0
- novacode_cli/vixie/server.py +375 -0
- novacode_cli/wiki/__init__.py +15 -0
- novacode_cli/wiki/ask.py +56 -0
- novacode_cli/wiki/file.py +71 -0
- novacode_cli/wiki/ingest.py +184 -0
- novacode_cli/wiki/manager.py +261 -0
- novacode_cli-1.2.0.dist-info/METADATA +271 -0
- novacode_cli-1.2.0.dist-info/RECORD +691 -0
- novacode_cli-1.2.0.dist-info/WHEEL +5 -0
- novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
- novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
- novacode_cli-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
"""Screens for managing stored provider and service credentials (``/auth``).
|
|
2
|
+
|
|
3
|
+
`AuthManagerScreen` lists every credential Nova can store, with a badge showing
|
|
4
|
+
where each one stands, and routes the user into `AuthPromptScreen`. The prompt
|
|
5
|
+
is the only place a credential is written, and the only place one is deleted
|
|
6
|
+
(after a confirmation) — the manager never writes, so there is one path to
|
|
7
|
+
audit.
|
|
8
|
+
|
|
9
|
+
Security notes:
|
|
10
|
+
|
|
11
|
+
- The key field is rendered with ``password=True`` so the value is never echoed
|
|
12
|
+
to the terminal.
|
|
13
|
+
- The key value is never logged, never included in a notification, and never
|
|
14
|
+
interpolated into an error message. Only the credential's *name* (its
|
|
15
|
+
environment variable) is ever shown.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import contextlib
|
|
22
|
+
import os
|
|
23
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
24
|
+
from urllib.parse import urlsplit
|
|
25
|
+
|
|
26
|
+
from rich.text import Text
|
|
27
|
+
from textual import work
|
|
28
|
+
from textual.containers import Horizontal, Vertical
|
|
29
|
+
from textual.screen import ModalScreen
|
|
30
|
+
from textual.widgets import Button, Input, OptionList, Static, Tab, Tabs
|
|
31
|
+
from textual.widgets.option_list import Option
|
|
32
|
+
|
|
33
|
+
from novacode_cli.config.credentials import (
|
|
34
|
+
CredentialMeta,
|
|
35
|
+
CredentialStore,
|
|
36
|
+
delete_credential,
|
|
37
|
+
has_stored_credential,
|
|
38
|
+
set_credential,
|
|
39
|
+
)
|
|
40
|
+
from novacode_cli.config.provider_auth import (
|
|
41
|
+
ProviderAuthSource,
|
|
42
|
+
ProviderAuthState,
|
|
43
|
+
ProviderAuthStatus,
|
|
44
|
+
credential_env_var,
|
|
45
|
+
credential_names,
|
|
46
|
+
credential_section,
|
|
47
|
+
get_all_auth_statuses,
|
|
48
|
+
is_service,
|
|
49
|
+
provider_display_name,
|
|
50
|
+
)
|
|
51
|
+
from novacode_cli.tui.animations import animate_modal_screen
|
|
52
|
+
from novacode_cli.tui.auth_display import format_auth_badge
|
|
53
|
+
from novacode_cli.tui.screens import ConfirmModal
|
|
54
|
+
|
|
55
|
+
if TYPE_CHECKING:
|
|
56
|
+
from textual.app import ComposeResult
|
|
57
|
+
from textual.binding import BindingType
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
#: What a *service* credential unlocks, for the rows that need to say it. Only a
|
|
61
|
+
#: service that gates a tool earns an entry: the voice credentials already name
|
|
62
|
+
#: their own axis in the display name ("Deepgram (speech to text)"), so reusing
|
|
63
|
+
#: Tavily's "gates web search" note for them was simply wrong.
|
|
64
|
+
_SERVICE_NOTES: dict[str, str] = {
|
|
65
|
+
"tavily": "gates web search",
|
|
66
|
+
"exa": "web search; connect its preset in /mcp",
|
|
67
|
+
"firecrawl": "web crawling; connect its preset in /mcp",
|
|
68
|
+
"parallel": "web research; connect its preset in /mcp",
|
|
69
|
+
"tinyfish": "browser automation; connect its preset in /mcp",
|
|
70
|
+
"jev": "decision-based tool pruning",
|
|
71
|
+
"systemone": "custom endpoint credential",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _endpoint_providers() -> frozenset[str]:
|
|
76
|
+
"""Return the providers that accept a custom endpoint.
|
|
77
|
+
|
|
78
|
+
Imported lazily so this module does not pull `model_manager` (and through it
|
|
79
|
+
the LangChain model classes) just to answer a gating question.
|
|
80
|
+
"""
|
|
81
|
+
from novacode_cli.config.model_manager import ENDPOINT_PROVIDERS
|
|
82
|
+
|
|
83
|
+
return ENDPOINT_PROVIDERS
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _is_http_url(value: str) -> bool:
|
|
87
|
+
"""Whether *value* looks like an http(s) URL.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
value: Candidate endpoint URL.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True when the scheme is http or https and a host is present.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
parts = urlsplit(value)
|
|
97
|
+
except ValueError:
|
|
98
|
+
return False
|
|
99
|
+
return parts.scheme in {"http", "https"} and bool(parts.netloc)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AuthManagerScreen(ModalScreen[None]):
|
|
103
|
+
"""List stored credentials and route the user into the key prompt.
|
|
104
|
+
|
|
105
|
+
Always dismisses with ``None``; the caller reads :attr:`saved` and
|
|
106
|
+
:attr:`deleted` afterwards to report what changed. State lives in the
|
|
107
|
+
credential store, so the list is re-read rather than patched after any
|
|
108
|
+
change.
|
|
109
|
+
|
|
110
|
+
Credential reads are filesystem/keychain work and run in a worker thread:
|
|
111
|
+
a status pass touches the OS credential store once per provider, which is
|
|
112
|
+
not something to do inside `compose`.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
116
|
+
("escape", "close", "Close"),
|
|
117
|
+
("ctrl+d", "delete", "Delete the stored key"),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
def __init__(self, *, focus: str | None = None) -> None:
|
|
121
|
+
"""Initialize the manager.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
focus: Credential name whose row should be highlighted once the
|
|
125
|
+
list is built — used when reopening so the cursor lands on the
|
|
126
|
+
row the user just worked on instead of resetting to the top.
|
|
127
|
+
"""
|
|
128
|
+
super().__init__()
|
|
129
|
+
self._focus = focus
|
|
130
|
+
self._section = credential_section(focus) if focus else "llms"
|
|
131
|
+
self._filter = ""
|
|
132
|
+
self._names: list[str] = []
|
|
133
|
+
self._statuses: dict[str, ProviderAuthStatus] = {}
|
|
134
|
+
self._metas: dict[str, CredentialMeta] = {}
|
|
135
|
+
#: Credential names saved or deleted while this screen was open, in the
|
|
136
|
+
#: order they happened. The caller reports them; the screen does not.
|
|
137
|
+
self.saved: list[str] = []
|
|
138
|
+
self.deleted: list[str] = []
|
|
139
|
+
self.model_settings_changed: list[str] = []
|
|
140
|
+
self.warnings: list[str] = []
|
|
141
|
+
|
|
142
|
+
def compose(self) -> ComposeResult:
|
|
143
|
+
"""Compose the manager list and its buttons."""
|
|
144
|
+
with Vertical(id="modal-box"):
|
|
145
|
+
yield Static(Text("Manage credentials", style="bold"), id="modal-title")
|
|
146
|
+
yield Static(
|
|
147
|
+
Text(
|
|
148
|
+
"Stored in your OS credential store, so they survive a restart "
|
|
149
|
+
"and apply to every Nova session.\n"
|
|
150
|
+
"Providers that need no key (Ollama) are not listed.",
|
|
151
|
+
style="dim",
|
|
152
|
+
),
|
|
153
|
+
id="auth-desc",
|
|
154
|
+
)
|
|
155
|
+
yield Tabs(
|
|
156
|
+
Tab("LLM Providers", id="llms"),
|
|
157
|
+
Tab("Tools", id="tools"),
|
|
158
|
+
Tab("Decisions", id="decisions"),
|
|
159
|
+
Tab("Voice", id="voice"),
|
|
160
|
+
active=self._section, id="auth-sections",
|
|
161
|
+
)
|
|
162
|
+
yield Input(placeholder="Search providers or credentials…", id="auth-filter")
|
|
163
|
+
yield OptionList(id="auth-list")
|
|
164
|
+
yield Static("", id="auth-hint")
|
|
165
|
+
with Horizontal(id="modal-buttons"):
|
|
166
|
+
yield Button("Add / Replace", id="edit", variant="primary")
|
|
167
|
+
yield Button("Delete", id="delete", variant="error")
|
|
168
|
+
yield Button("Close", id="close")
|
|
169
|
+
|
|
170
|
+
def on_mount(self) -> None:
|
|
171
|
+
"""Animate in and load the credential list."""
|
|
172
|
+
animate_modal_screen(self)
|
|
173
|
+
self.reload()
|
|
174
|
+
|
|
175
|
+
@work(thread=True, group="auth_reload", exclusive=True)
|
|
176
|
+
def reload(self) -> None:
|
|
177
|
+
"""Re-read credentials and repaint, off the UI thread."""
|
|
178
|
+
try:
|
|
179
|
+
statuses = get_all_auth_statuses(include_providers_without_keys=True)
|
|
180
|
+
except Exception: # noqa: BLE001 — a failed pass must still render a list
|
|
181
|
+
statuses = {}
|
|
182
|
+
# Suppressed, not handled: the screen is dismissed before the background
|
|
183
|
+
# pass finishes whenever the user closes the manager quickly, and there
|
|
184
|
+
# is nothing left to repaint by then.
|
|
185
|
+
with contextlib.suppress(Exception):
|
|
186
|
+
self.app.call_from_thread(self._paint, statuses)
|
|
187
|
+
|
|
188
|
+
def _paint(self, statuses: dict[str, ProviderAuthStatus]) -> None:
|
|
189
|
+
"""Rebuild the option list from *statuses*.
|
|
190
|
+
|
|
191
|
+
Named `_paint`, not `_render`: `Widget._render` is part of Textual's own
|
|
192
|
+
render pipeline (it returns a Visual) and shadowing it made the screen
|
|
193
|
+
raise on every frame.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
statuses: Readiness per provider/service id.
|
|
197
|
+
"""
|
|
198
|
+
self._statuses = statuses
|
|
199
|
+
names = [
|
|
200
|
+
name for name in (*credential_names(), "google-vertex")
|
|
201
|
+
if name in statuses and credential_section(name) == self._section
|
|
202
|
+
and self._filter.casefold() in (
|
|
203
|
+
f"{name} {provider_display_name(name)} {credential_env_var(name)}".casefold()
|
|
204
|
+
)
|
|
205
|
+
]
|
|
206
|
+
self._names = names
|
|
207
|
+
|
|
208
|
+
self._metas = CredentialStore().list_meta(
|
|
209
|
+
env_var for env_var in (credential_env_var(name) for name in names) if env_var
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
option_list = self.query_one("#auth-list", OptionList)
|
|
213
|
+
option_list.clear_options()
|
|
214
|
+
for name in names:
|
|
215
|
+
option_list.add_option(Option(self._row_label(name, statuses[name]), id=name))
|
|
216
|
+
|
|
217
|
+
if not names:
|
|
218
|
+
message = (
|
|
219
|
+
"No credentials match your search." if self._filter
|
|
220
|
+
else "No credentials are available in this section."
|
|
221
|
+
)
|
|
222
|
+
option_list.add_option(Option(message, id="__none__"))
|
|
223
|
+
|
|
224
|
+
if self._focus and self._focus in names:
|
|
225
|
+
option_list.highlighted = names.index(self._focus)
|
|
226
|
+
option_list.focus()
|
|
227
|
+
self._update_hint()
|
|
228
|
+
|
|
229
|
+
def on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None:
|
|
230
|
+
"""Keep tools, decisions, voice, and model credentials in their own sections."""
|
|
231
|
+
if event.tabs.id == "auth-sections" and event.tab.id and event.tab.id != self._section:
|
|
232
|
+
self._section = event.tab.id
|
|
233
|
+
self._focus = None
|
|
234
|
+
self._paint(self._statuses)
|
|
235
|
+
|
|
236
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
237
|
+
"""Filter the selected section without changing credentials or starting work."""
|
|
238
|
+
if event.input.id == "auth-filter" and event.value != self._filter:
|
|
239
|
+
self._filter = event.value
|
|
240
|
+
self._paint(self._statuses)
|
|
241
|
+
event.input.focus()
|
|
242
|
+
|
|
243
|
+
def _row_label(self, name: str, status: ProviderAuthStatus) -> Text:
|
|
244
|
+
"""Build one row: display name, badge, and a short status detail.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
name: Provider or service id.
|
|
248
|
+
status: Its readiness status.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
The composed row text.
|
|
252
|
+
"""
|
|
253
|
+
display = provider_display_name(name)
|
|
254
|
+
text = Text()
|
|
255
|
+
text.append(display, style="bold")
|
|
256
|
+
text.append(" ")
|
|
257
|
+
text.append_text(format_auth_badge(status))
|
|
258
|
+
detail = self._row_detail(name, status)
|
|
259
|
+
if detail:
|
|
260
|
+
text.append(f" {detail}", style="dim")
|
|
261
|
+
return text
|
|
262
|
+
|
|
263
|
+
def _row_detail(self, name: str, status: ProviderAuthStatus) -> str:
|
|
264
|
+
"""Return the trailing description shown on a row.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
name: Provider or service id.
|
|
268
|
+
status: Its readiness status.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
A short detail string, or an empty string when the badge says enough.
|
|
272
|
+
"""
|
|
273
|
+
if status.state is ProviderAuthState.CONFIGURED:
|
|
274
|
+
env_var = credential_env_var(name)
|
|
275
|
+
meta = self._metas.get(env_var) if env_var else None
|
|
276
|
+
parts = []
|
|
277
|
+
if meta is not None and meta.base_url:
|
|
278
|
+
parts.append(meta.base_url)
|
|
279
|
+
if is_service(name):
|
|
280
|
+
note = _SERVICE_NOTES.get(name)
|
|
281
|
+
if note:
|
|
282
|
+
parts.append(note)
|
|
283
|
+
if status.detail in {"ChatGPT plan sign-in", "Google OAuth sign-in"}:
|
|
284
|
+
parts.append(status.detail)
|
|
285
|
+
return " · ".join(parts)
|
|
286
|
+
return status.detail or ""
|
|
287
|
+
|
|
288
|
+
def _highlighted(self) -> str | None:
|
|
289
|
+
"""Return the highlighted credential name, if it is a real row."""
|
|
290
|
+
option_list = self.query_one("#auth-list", OptionList)
|
|
291
|
+
index = option_list.highlighted
|
|
292
|
+
if index is None or not (0 <= index < len(self._names)):
|
|
293
|
+
return None
|
|
294
|
+
return self._names[index]
|
|
295
|
+
|
|
296
|
+
def _update_hint(self) -> None:
|
|
297
|
+
"""Refresh the footer for the highlighted row."""
|
|
298
|
+
name = self._highlighted()
|
|
299
|
+
hint = self.query_one("#auth-hint", Static)
|
|
300
|
+
managed = name is None or credential_env_var(name) is None
|
|
301
|
+
self.query_one("#edit", Button).disabled = managed
|
|
302
|
+
self.query_one("#delete", Button).disabled = managed
|
|
303
|
+
if name is None:
|
|
304
|
+
hint.update(Text("Esc close", style="dim"))
|
|
305
|
+
return
|
|
306
|
+
if credential_env_var(name) is None:
|
|
307
|
+
hint.update(Text(
|
|
308
|
+
"Configure Google Cloud Application Default Credentials outside Nova."
|
|
309
|
+
" · Esc close",
|
|
310
|
+
style="dim",
|
|
311
|
+
))
|
|
312
|
+
return
|
|
313
|
+
action = "replace" if self._stored(name) else "add"
|
|
314
|
+
deletable = " · Ctrl+D delete" if self._stored(name) else ""
|
|
315
|
+
hint.update(Text(f"Enter {action} the key{deletable} · Esc close", style="dim"))
|
|
316
|
+
|
|
317
|
+
def _stored(self, name: str) -> bool:
|
|
318
|
+
"""Whether a credential for *name* came from the credential store.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
name: Provider or service id.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
True when `/auth` can replace or delete it.
|
|
325
|
+
"""
|
|
326
|
+
status = self._statuses.get(name)
|
|
327
|
+
return status is not None and status.source is ProviderAuthSource.STORED
|
|
328
|
+
|
|
329
|
+
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
330
|
+
"""Open the prompt for the selected credential."""
|
|
331
|
+
if event.option_list.id == "auth-list":
|
|
332
|
+
self.open_prompt()
|
|
333
|
+
|
|
334
|
+
def on_option_list_option_highlighted(self, event: OptionList.OptionHighlighted) -> None:
|
|
335
|
+
"""Update the footer when the highlight moves."""
|
|
336
|
+
if event.option_list.id == "auth-list":
|
|
337
|
+
self._update_hint()
|
|
338
|
+
|
|
339
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
340
|
+
"""Route the manager's buttons."""
|
|
341
|
+
if event.button.id == "close":
|
|
342
|
+
self.dismiss(None)
|
|
343
|
+
elif event.button.id == "edit":
|
|
344
|
+
self.open_prompt()
|
|
345
|
+
elif event.button.id == "delete":
|
|
346
|
+
self.remove_selected()
|
|
347
|
+
|
|
348
|
+
@work(group="auth_action", exclusive=True)
|
|
349
|
+
async def open_prompt(self) -> None:
|
|
350
|
+
"""Push the key prompt for the highlighted credential and reload."""
|
|
351
|
+
name = self._highlighted()
|
|
352
|
+
if name is None:
|
|
353
|
+
return
|
|
354
|
+
env_var = credential_env_var(name)
|
|
355
|
+
if env_var is None:
|
|
356
|
+
return
|
|
357
|
+
if name in {"google", "anthropic", "opencode", "opencode_zen", "commandcode"}:
|
|
358
|
+
from novacode_cli.tui.provider_signin import (
|
|
359
|
+
GoogleSignInScreen,
|
|
360
|
+
ProviderSignInChoice,
|
|
361
|
+
open_anthropic_console,
|
|
362
|
+
open_commandcode_studio,
|
|
363
|
+
open_opencode_console,
|
|
364
|
+
)
|
|
365
|
+
choice = await self.app.push_screen_wait(ProviderSignInChoice(name))
|
|
366
|
+
if choice == "zdr":
|
|
367
|
+
from novacode_cli.config.nova_config import NovaConfig, _config_bool
|
|
368
|
+
|
|
369
|
+
config = NovaConfig()
|
|
370
|
+
on = not _config_bool(config.get("commandcode_zdr", False))
|
|
371
|
+
config.set("commandcode_zdr", on)
|
|
372
|
+
self.model_settings_changed.append(name)
|
|
373
|
+
self.app.notify(
|
|
374
|
+
f"Command Code zero data retention {'on' if on else 'off'}. "
|
|
375
|
+
"Close /auth to refresh the model."
|
|
376
|
+
)
|
|
377
|
+
return
|
|
378
|
+
if name == "google":
|
|
379
|
+
from novacode_cli.config import google_oauth_auth as google
|
|
380
|
+
if choice == "signin":
|
|
381
|
+
if await self.app.push_screen_wait(GoogleSignInScreen()):
|
|
382
|
+
self.saved.append(name)
|
|
383
|
+
self._focus = name
|
|
384
|
+
self.reload()
|
|
385
|
+
self.app.notify("Gemini Google sign-in connected.")
|
|
386
|
+
return
|
|
387
|
+
if choice == "use":
|
|
388
|
+
google.set_auth_mode("oauth")
|
|
389
|
+
self.saved.append(name)
|
|
390
|
+
self.reload()
|
|
391
|
+
return
|
|
392
|
+
if choice == "remove":
|
|
393
|
+
confirmed = await self.app.push_screen_wait(ConfirmModal(
|
|
394
|
+
"Disconnect Google sign-in?",
|
|
395
|
+
Text("Removes Nova's stored Google tokens. Your API key is retained."),
|
|
396
|
+
))
|
|
397
|
+
if confirmed and await asyncio.to_thread(google.delete_credentials):
|
|
398
|
+
google.set_auth_mode("auto")
|
|
399
|
+
self.deleted.append(name)
|
|
400
|
+
self.reload()
|
|
401
|
+
return
|
|
402
|
+
if choice == "api_key" and (
|
|
403
|
+
CredentialStore().get_key(env_var) or os.environ.get(env_var)
|
|
404
|
+
or os.environ.get("GEMINI_API_KEY")
|
|
405
|
+
):
|
|
406
|
+
google.set_auth_mode("api_key")
|
|
407
|
+
self.saved.append(name)
|
|
408
|
+
self.reload()
|
|
409
|
+
return
|
|
410
|
+
elif choice == "console":
|
|
411
|
+
if name in {"opencode", "opencode_zen"}:
|
|
412
|
+
await open_opencode_console()
|
|
413
|
+
elif name == "commandcode":
|
|
414
|
+
await open_commandcode_studio()
|
|
415
|
+
else:
|
|
416
|
+
await open_anthropic_console()
|
|
417
|
+
if choice not in {"api_key", "replace_key", "console"}:
|
|
418
|
+
return
|
|
419
|
+
if name == "openai":
|
|
420
|
+
choice = await self.app.push_screen_wait(OpenAIAuthChoiceScreen())
|
|
421
|
+
if choice == "chatgpt":
|
|
422
|
+
try:
|
|
423
|
+
from novacode_cli.config.openai_chatgpt_auth import sign_in
|
|
424
|
+
|
|
425
|
+
await asyncio.to_thread(sign_in)
|
|
426
|
+
from novacode_cli.config.openai_chatgpt_auth import set_auth_mode
|
|
427
|
+
|
|
428
|
+
set_auth_mode("chatgpt")
|
|
429
|
+
except Exception as exc: # noqa: BLE001 — keep token details out of UI
|
|
430
|
+
self.app.notify(str(exc), severity="error", markup=False)
|
|
431
|
+
return
|
|
432
|
+
self.saved.append(name)
|
|
433
|
+
self._focus = name
|
|
434
|
+
self.reload()
|
|
435
|
+
self.app.notify("OpenAI ChatGPT sign-in connected.", severity="information")
|
|
436
|
+
return
|
|
437
|
+
if choice == "use_chatgpt":
|
|
438
|
+
from novacode_cli.config.openai_chatgpt_auth import set_auth_mode
|
|
439
|
+
|
|
440
|
+
set_auth_mode("chatgpt")
|
|
441
|
+
self.saved.append(name)
|
|
442
|
+
self._focus = name
|
|
443
|
+
self.reload()
|
|
444
|
+
self.app.notify("OpenAI ChatGPT sign-in selected.", severity="information")
|
|
445
|
+
return
|
|
446
|
+
if choice == "remove_chatgpt":
|
|
447
|
+
from novacode_cli.config.openai_chatgpt_auth import (
|
|
448
|
+
delete_credentials,
|
|
449
|
+
load_credentials,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
record = load_credentials()
|
|
453
|
+
if record:
|
|
454
|
+
email = record.get("email") or "this ChatGPT account"
|
|
455
|
+
confirmed = await self.app.push_screen_wait(
|
|
456
|
+
ConfirmModal(
|
|
457
|
+
"Remove the ChatGPT sign-in?",
|
|
458
|
+
Text(
|
|
459
|
+
f"This removes Nova's stored access for {email} "
|
|
460
|
+
"from the OS credential store."
|
|
461
|
+
),
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
if confirmed and delete_credentials():
|
|
465
|
+
from novacode_cli.config.openai_chatgpt_auth import (
|
|
466
|
+
selected_auth_mode,
|
|
467
|
+
set_auth_mode,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
if selected_auth_mode() == "chatgpt":
|
|
471
|
+
set_auth_mode("auto")
|
|
472
|
+
self.deleted.append(name)
|
|
473
|
+
self.reload()
|
|
474
|
+
return
|
|
475
|
+
if choice == "api_key":
|
|
476
|
+
from novacode_cli.config.openai_chatgpt_auth import set_auth_mode
|
|
477
|
+
|
|
478
|
+
if CredentialStore().get_key(env_var) or os.environ.get(env_var):
|
|
479
|
+
set_auth_mode("api_key")
|
|
480
|
+
self.saved.append(name)
|
|
481
|
+
self._focus = name
|
|
482
|
+
self.reload()
|
|
483
|
+
self.app.notify("OpenAI API-key auth selected.", severity="information")
|
|
484
|
+
return
|
|
485
|
+
elif choice != "replace_key":
|
|
486
|
+
return
|
|
487
|
+
prompt = AuthPromptScreen(name, existing=self._metas.get(env_var))
|
|
488
|
+
result = await self.app.push_screen_wait(prompt)
|
|
489
|
+
if prompt.warnings:
|
|
490
|
+
self.warnings.extend(prompt.warnings)
|
|
491
|
+
for warning in prompt.warnings:
|
|
492
|
+
# markup=False: the text interpolates an env var name and a path
|
|
493
|
+
# step that may contain characters Textual's markup reads as tags.
|
|
494
|
+
self.app.notify(warning, severity="warning", markup=False)
|
|
495
|
+
if result:
|
|
496
|
+
if name == "google":
|
|
497
|
+
from novacode_cli.config.google_oauth_auth import set_auth_mode
|
|
498
|
+
set_auth_mode("api_key")
|
|
499
|
+
if name == "openai":
|
|
500
|
+
from novacode_cli.config.openai_chatgpt_auth import set_auth_mode
|
|
501
|
+
|
|
502
|
+
set_auth_mode("api_key")
|
|
503
|
+
self.saved.append(name)
|
|
504
|
+
self._focus = name
|
|
505
|
+
self.reload()
|
|
506
|
+
|
|
507
|
+
@work(group="auth_action", exclusive=True)
|
|
508
|
+
async def remove_selected(self) -> None:
|
|
509
|
+
"""Confirm, then delete the highlighted credential."""
|
|
510
|
+
name = self._highlighted()
|
|
511
|
+
if name is None:
|
|
512
|
+
return
|
|
513
|
+
env_var = credential_env_var(name)
|
|
514
|
+
if env_var is None:
|
|
515
|
+
return
|
|
516
|
+
if name == "openai" and not has_stored_credential(env_var):
|
|
517
|
+
from novacode_cli.config.openai_chatgpt_auth import (
|
|
518
|
+
delete_credentials,
|
|
519
|
+
load_credentials,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
record = load_credentials()
|
|
523
|
+
if record:
|
|
524
|
+
email = record.get("email") or "this ChatGPT account"
|
|
525
|
+
confirmed = await self.app.push_screen_wait(
|
|
526
|
+
ConfirmModal(
|
|
527
|
+
"Remove the ChatGPT sign-in?",
|
|
528
|
+
Text(
|
|
529
|
+
f"This removes Nova's stored access for {email} "
|
|
530
|
+
"from the OS credential store."
|
|
531
|
+
),
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
if confirmed and delete_credentials():
|
|
535
|
+
from novacode_cli.config.openai_chatgpt_auth import (
|
|
536
|
+
selected_auth_mode,
|
|
537
|
+
set_auth_mode,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
if selected_auth_mode() == "chatgpt":
|
|
541
|
+
set_auth_mode("auto")
|
|
542
|
+
self.deleted.append(name)
|
|
543
|
+
self._focus = name
|
|
544
|
+
self.reload()
|
|
545
|
+
return
|
|
546
|
+
if not self._stored(name) or not has_stored_credential(env_var):
|
|
547
|
+
# Deleting an environment-supplied key is not something `/auth` can
|
|
548
|
+
# do, and pretending otherwise would leave the user thinking it is
|
|
549
|
+
# gone. Say so instead.
|
|
550
|
+
self.app.notify(
|
|
551
|
+
f"{self._statuses[name].env_var or env_var} is not stored here "
|
|
552
|
+
"— it comes from your environment. "
|
|
553
|
+
f"Unset it in your shell or .env instead.",
|
|
554
|
+
severity="warning",
|
|
555
|
+
markup=False,
|
|
556
|
+
)
|
|
557
|
+
return
|
|
558
|
+
confirmed = await self.app.push_screen_wait(
|
|
559
|
+
ConfirmModal(
|
|
560
|
+
f"Delete the stored key for {provider_display_name(name)}?",
|
|
561
|
+
Text(f"This removes {env_var} from your credential store. It cannot be undone."),
|
|
562
|
+
)
|
|
563
|
+
)
|
|
564
|
+
if not confirmed:
|
|
565
|
+
return
|
|
566
|
+
if delete_credential(env_var):
|
|
567
|
+
self.deleted.append(name)
|
|
568
|
+
else:
|
|
569
|
+
self.app.notify(
|
|
570
|
+
f"Could not delete the credential for {env_var}.",
|
|
571
|
+
severity="error",
|
|
572
|
+
markup=False,
|
|
573
|
+
)
|
|
574
|
+
self._focus = name
|
|
575
|
+
self.reload()
|
|
576
|
+
|
|
577
|
+
def action_close(self) -> None:
|
|
578
|
+
"""Dismiss the manager."""
|
|
579
|
+
self.dismiss(None)
|
|
580
|
+
|
|
581
|
+
def action_delete(self) -> None:
|
|
582
|
+
"""Delete the highlighted credential, after confirmation."""
|
|
583
|
+
self.remove_selected()
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
class OpenAIAuthChoiceScreen(ModalScreen[str | None]):
|
|
587
|
+
"""Choose an API key or Sign in with ChatGPT for OpenAI."""
|
|
588
|
+
|
|
589
|
+
def compose(self) -> ComposeResult:
|
|
590
|
+
from novacode_cli.config.openai_chatgpt_auth import (
|
|
591
|
+
load_credentials,
|
|
592
|
+
selected_auth_mode,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
record = load_credentials()
|
|
596
|
+
mode = selected_auth_mode()
|
|
597
|
+
mode_label = {"chatgpt": "ChatGPT", "api_key": "API key"}.get(mode, "automatic")
|
|
598
|
+
details = [f"Active auth: {mode_label}"]
|
|
599
|
+
if record:
|
|
600
|
+
details.insert(0, f"Connected: {record.get('email') or 'ChatGPT account'}")
|
|
601
|
+
detail = "\n" + "\n".join(details)
|
|
602
|
+
with Vertical(id="modal-box"):
|
|
603
|
+
yield Static(Text("Connect OpenAI", style="bold"), id="modal-title")
|
|
604
|
+
yield Static(
|
|
605
|
+
Text(
|
|
606
|
+
"Select which OpenAI credential Nova should use. API keys use "
|
|
607
|
+
"OpenAI API billing; ChatGPT sign-in authorizes eligible Responses API usage."
|
|
608
|
+
"\nChatGPT sign-in has preview limits: some hosted tools are unavailable."
|
|
609
|
+
+ detail,
|
|
610
|
+
style="dim",
|
|
611
|
+
),
|
|
612
|
+
id="auth-desc",
|
|
613
|
+
)
|
|
614
|
+
with Horizontal(id="modal-buttons"):
|
|
615
|
+
yield Button("Use API key", id="api-key", variant="primary")
|
|
616
|
+
yield Button("Add / replace API key", id="replace-key")
|
|
617
|
+
yield Button("Continue with ChatGPT", id="chatgpt")
|
|
618
|
+
if record:
|
|
619
|
+
yield Button("Use saved ChatGPT account", id="use-chatgpt")
|
|
620
|
+
yield Button("Remove ChatGPT sign-in", id="remove-chatgpt", variant="error")
|
|
621
|
+
yield Button("Cancel", id="cancel")
|
|
622
|
+
|
|
623
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
624
|
+
choices = {
|
|
625
|
+
"api-key": "api_key",
|
|
626
|
+
"replace-key": "replace_key",
|
|
627
|
+
"chatgpt": "chatgpt",
|
|
628
|
+
"use-chatgpt": "use_chatgpt",
|
|
629
|
+
"remove-chatgpt": "remove_chatgpt",
|
|
630
|
+
}
|
|
631
|
+
self.dismiss(choices.get(event.button.id))
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class AuthPromptScreen(ModalScreen[str | None]):
|
|
635
|
+
"""Accept (or delete) the credential for one provider or service.
|
|
636
|
+
|
|
637
|
+
Dismisses with the credential name when it was saved, or ``None`` when the
|
|
638
|
+
user cancelled. Warnings from the write are left on :attr:`warnings` for the
|
|
639
|
+
caller to surface: a keychain write that failed partially is the one signal
|
|
640
|
+
the user must not miss, and a toast raised from inside a dismissing screen
|
|
641
|
+
competes with its own teardown.
|
|
642
|
+
"""
|
|
643
|
+
|
|
644
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
645
|
+
("escape", "cancel", "Cancel"),
|
|
646
|
+
("f2", "toggle_advanced", "Advanced"),
|
|
647
|
+
("ctrl+d", "delete", "Delete key"),
|
|
648
|
+
]
|
|
649
|
+
|
|
650
|
+
def __init__(self, name: str, *, existing: CredentialMeta | None = None) -> None:
|
|
651
|
+
"""Initialize the prompt.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
name: Provider or service id whose credential is being set.
|
|
655
|
+
existing: Metadata already recorded for this credential, used to
|
|
656
|
+
prefill the endpoint so a rotation does not silently drop it.
|
|
657
|
+
"""
|
|
658
|
+
super().__init__()
|
|
659
|
+
self._name = name
|
|
660
|
+
self._existing = existing
|
|
661
|
+
# Azure has no default endpoint, so its field starts open.
|
|
662
|
+
self._advanced = name == "azure"
|
|
663
|
+
self._warnings: list[str] = []
|
|
664
|
+
|
|
665
|
+
@property
|
|
666
|
+
def warnings(self) -> list[str]:
|
|
667
|
+
"""Warnings produced by a save that nonetheless stored the key."""
|
|
668
|
+
return list(self._warnings)
|
|
669
|
+
|
|
670
|
+
@property
|
|
671
|
+
def _env_var(self) -> str | None:
|
|
672
|
+
"""The environment variable this credential is filed under."""
|
|
673
|
+
return credential_env_var(self._name)
|
|
674
|
+
|
|
675
|
+
@property
|
|
676
|
+
def _supports_endpoint(self) -> bool:
|
|
677
|
+
"""Whether this credential's provider accepts a custom endpoint."""
|
|
678
|
+
return self._name in _endpoint_providers()
|
|
679
|
+
|
|
680
|
+
def compose(self) -> ComposeResult:
|
|
681
|
+
"""Compose the key prompt, with the advanced panel hidden."""
|
|
682
|
+
env_var = self._env_var or self._name
|
|
683
|
+
with Vertical(id="modal-box"):
|
|
684
|
+
yield Static(
|
|
685
|
+
Text(f"{provider_display_name(self._name)} API key", style="bold"),
|
|
686
|
+
id="modal-title",
|
|
687
|
+
)
|
|
688
|
+
yield Static(
|
|
689
|
+
Text(
|
|
690
|
+
f"Paste the key below. It is stored in your OS credential store as "
|
|
691
|
+
f"{env_var} and applied to this session immediately."
|
|
692
|
+
+ (
|
|
693
|
+
"\nConfigure decision models in /model → Decisions."
|
|
694
|
+
if self._name in {"jev", "systemone"}
|
|
695
|
+
else ""
|
|
696
|
+
),
|
|
697
|
+
style="dim",
|
|
698
|
+
),
|
|
699
|
+
id="auth-desc",
|
|
700
|
+
)
|
|
701
|
+
yield Input(placeholder="API key (input is hidden)", password=True, id="auth-key")
|
|
702
|
+
yield Static(
|
|
703
|
+
Text(
|
|
704
|
+
"Enter save · F2 advanced · Ctrl+D delete stored key · Esc cancel",
|
|
705
|
+
style="dim",
|
|
706
|
+
),
|
|
707
|
+
id="auth-hint",
|
|
708
|
+
)
|
|
709
|
+
yield Static("", id="auth-error")
|
|
710
|
+
yield Static(
|
|
711
|
+
Text(
|
|
712
|
+
"Endpoint URL (your Azure resource)"
|
|
713
|
+
if self._name == "azure"
|
|
714
|
+
else "Endpoint URL (blank = provider default)",
|
|
715
|
+
style="bold",
|
|
716
|
+
),
|
|
717
|
+
id="auth-endpoint-label",
|
|
718
|
+
)
|
|
719
|
+
yield Input(
|
|
720
|
+
placeholder=(
|
|
721
|
+
"https://<resource>.openai.azure.com" if self._name == "azure" else "https://…"
|
|
722
|
+
),
|
|
723
|
+
id="auth-endpoint",
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
def on_mount(self) -> None:
|
|
727
|
+
"""Hide the advanced panel and focus the key field."""
|
|
728
|
+
animate_modal_screen(self)
|
|
729
|
+
self._apply_advanced_visibility()
|
|
730
|
+
if self._advanced and self._existing is not None and self._existing.base_url:
|
|
731
|
+
self.query_one("#auth-endpoint", Input).value = self._existing.base_url
|
|
732
|
+
self.query_one("#auth-key", Input).focus()
|
|
733
|
+
|
|
734
|
+
def _apply_advanced_visibility(self) -> None:
|
|
735
|
+
"""Show the endpoint field only when advanced is open and it applies."""
|
|
736
|
+
show = self._advanced and self._supports_endpoint
|
|
737
|
+
self.query_one("#auth-endpoint-label", Static).display = show
|
|
738
|
+
self.query_one("#auth-endpoint", Input).display = show
|
|
739
|
+
|
|
740
|
+
def action_toggle_advanced(self) -> None:
|
|
741
|
+
"""Show or hide the endpoint field."""
|
|
742
|
+
self._advanced = not self._advanced
|
|
743
|
+
self._apply_advanced_visibility()
|
|
744
|
+
if self._advanced and self._supports_endpoint:
|
|
745
|
+
endpoint = self.query_one("#auth-endpoint", Input)
|
|
746
|
+
if not endpoint.value and self._existing is not None and self._existing.base_url:
|
|
747
|
+
# Prefill the stored endpoint so it is visible and editable
|
|
748
|
+
# rather than silently still in effect.
|
|
749
|
+
endpoint.value = self._existing.base_url
|
|
750
|
+
endpoint.focus()
|
|
751
|
+
else:
|
|
752
|
+
self.query_one("#auth-key", Input).focus()
|
|
753
|
+
|
|
754
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
755
|
+
"""Save on Enter from either field."""
|
|
756
|
+
event.stop()
|
|
757
|
+
self._save()
|
|
758
|
+
|
|
759
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
760
|
+
"""Route the prompt's buttons."""
|
|
761
|
+
if event.button.id == "cancel":
|
|
762
|
+
self.dismiss(None)
|
|
763
|
+
else:
|
|
764
|
+
self._save()
|
|
765
|
+
|
|
766
|
+
def _show_error(self, message: str) -> None:
|
|
767
|
+
"""Render an error under the key field."""
|
|
768
|
+
self.query_one("#auth-error", Static).update(Text(message, style="red"))
|
|
769
|
+
|
|
770
|
+
def _save(self) -> None:
|
|
771
|
+
"""Validate and persist the credential."""
|
|
772
|
+
env_var = self._env_var
|
|
773
|
+
if env_var is None:
|
|
774
|
+
self._show_error("This credential cannot be stored.")
|
|
775
|
+
return
|
|
776
|
+
|
|
777
|
+
key = self.query_one("#auth-key", Input).value.strip()
|
|
778
|
+
if not key:
|
|
779
|
+
self._show_error("API key cannot be empty.")
|
|
780
|
+
return
|
|
781
|
+
|
|
782
|
+
base_url = ""
|
|
783
|
+
if self._supports_endpoint:
|
|
784
|
+
base_url = self.query_one("#auth-endpoint", Input).value.strip()
|
|
785
|
+
existing_url = (self._existing.base_url if self._existing else "") or ""
|
|
786
|
+
# Validate only a *changed* value. The field is prefilled with the
|
|
787
|
+
# stored endpoint, so a legacy non-http(s) value must not block
|
|
788
|
+
# rotating just the key.
|
|
789
|
+
if base_url and base_url != existing_url and not _is_http_url(base_url):
|
|
790
|
+
self._show_error("Endpoint URL must be an http(s) URL.")
|
|
791
|
+
return
|
|
792
|
+
|
|
793
|
+
outcome = set_credential(env_var, key, base_url=base_url or None)
|
|
794
|
+
if not outcome.ok:
|
|
795
|
+
# The store reported the write failed; the message names the env var
|
|
796
|
+
# only, never the value.
|
|
797
|
+
self._warnings.extend(outcome.warnings)
|
|
798
|
+
self._show_error(outcome.warnings[0] if outcome.warnings else "Could not save.")
|
|
799
|
+
return
|
|
800
|
+
|
|
801
|
+
self._warnings.extend(outcome.warnings)
|
|
802
|
+
self.dismiss(self._name)
|
|
803
|
+
|
|
804
|
+
@work(exclusive=True)
|
|
805
|
+
async def action_delete(self) -> None:
|
|
806
|
+
"""Delete the stored credential after a confirmation."""
|
|
807
|
+
env_var = self._env_var
|
|
808
|
+
if env_var is None:
|
|
809
|
+
return
|
|
810
|
+
if not has_stored_credential(env_var):
|
|
811
|
+
self._show_error("No stored key to delete for this credential.")
|
|
812
|
+
return
|
|
813
|
+
confirmed = await self.app.push_screen_wait(
|
|
814
|
+
ConfirmModal(
|
|
815
|
+
f"Delete the stored key for {provider_display_name(self._name)}?",
|
|
816
|
+
Text(f"This removes {env_var} from your credential store. It cannot be undone."),
|
|
817
|
+
)
|
|
818
|
+
)
|
|
819
|
+
if not confirmed:
|
|
820
|
+
return
|
|
821
|
+
delete_credential(env_var)
|
|
822
|
+
self.dismiss(self._name)
|
|
823
|
+
|
|
824
|
+
def action_cancel(self) -> None:
|
|
825
|
+
"""Discard the prompt."""
|
|
826
|
+
self.dismiss(None)
|