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,540 @@
|
|
|
1
|
+
"""Credential storage for model providers and non-model services.
|
|
2
|
+
|
|
3
|
+
Credentials entered in the TUI have to survive a restart, be readable by more
|
|
4
|
+
than one process, and be deletable. Nova already has that storage —
|
|
5
|
+
:class:`novacode_cli.onboarding.SecretManager` (the OS keychain, with a
|
|
6
|
+
permission-restricted JSON fallback) — so this module does not introduce a
|
|
7
|
+
second secret backend. It adds the two things the keychain alone cannot express:
|
|
8
|
+
|
|
9
|
+
1. **An endpoint and a project paired with the key.** ``SecretManager`` stores
|
|
10
|
+
one opaque string per name, so the endpoint a personal key belongs to has
|
|
11
|
+
nowhere to live. It is kept beside the key in ``Nova.config.json`` as
|
|
12
|
+
non-secret metadata, never in the keychain, and never mixed into the secret
|
|
13
|
+
value (every existing reader copies that value straight into ``os.environ``,
|
|
14
|
+
so a JSON-wrapped value would poison the environment).
|
|
15
|
+
|
|
16
|
+
2. **Enumeration and deletion.** ``keyring`` cannot list entries, so a caller
|
|
17
|
+
must say which names to probe; :meth:`CredentialStore.list_meta` does exactly
|
|
18
|
+
that over a caller-supplied name set. A key stored under a name the
|
|
19
|
+
registry does not know about is therefore invisible here — the same
|
|
20
|
+
limitation ``SecretManager.list_secrets`` already documents.
|
|
21
|
+
|
|
22
|
+
Secrets are never logged, formatted with ``!r``, or interpolated into an
|
|
23
|
+
exception message; every helper reports structural facts only
|
|
24
|
+
("set credential for ANTHROPIC_API_KEY"). Failures are returned in
|
|
25
|
+
:class:`WriteOutcome` rather than raised, because ``logger.warning`` is
|
|
26
|
+
invisible inside a Textual session — the UI has to render them.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from datetime import UTC, datetime
|
|
35
|
+
from typing import TYPE_CHECKING
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from collections.abc import Iterable
|
|
39
|
+
|
|
40
|
+
from novacode_cli.onboarding import SecretManager
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _set_live_key(env_var: str, key: str | None) -> None:
|
|
46
|
+
"""Keep startup settings and the environment consistent after an auth edit."""
|
|
47
|
+
from novacode_cli.config.config import settings
|
|
48
|
+
|
|
49
|
+
if key is None:
|
|
50
|
+
os.environ.pop(env_var, None)
|
|
51
|
+
else:
|
|
52
|
+
os.environ[env_var] = key
|
|
53
|
+
field = env_var.lower()
|
|
54
|
+
if field.endswith("_api_key") and hasattr(settings, field):
|
|
55
|
+
setattr(settings, field, key)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class CredentialMeta:
|
|
60
|
+
"""The non-secret facts recorded alongside a stored credential.
|
|
61
|
+
|
|
62
|
+
Keyed by environment variable name, not by provider, because that is the
|
|
63
|
+
stable identifier the secret itself is filed under.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
env_var: The environment variable the credential is read as.
|
|
67
|
+
base_url: Endpoint paired with the key, or ``None`` for the provider
|
|
68
|
+
default. Only meaningful for providers that accept a custom
|
|
69
|
+
endpoint.
|
|
70
|
+
project: Optional project/workspace name paired with the key.
|
|
71
|
+
added_at: ISO-8601 UTC timestamp of the last write.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
env_var: str
|
|
75
|
+
base_url: str | None = None
|
|
76
|
+
project: str | None = None
|
|
77
|
+
added_at: str | None = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class WriteOutcome:
|
|
82
|
+
"""Result of a credential write.
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
ok: Whether the secret itself reached the store. Metadata failures are
|
|
86
|
+
still reported as warnings on a successful write, because a lost
|
|
87
|
+
endpoint is a broken pairing, not a lost credential.
|
|
88
|
+
warnings: Problems the user must see — chiefly a keychain write that
|
|
89
|
+
reported failure. Empty on a clean write.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
ok: bool
|
|
93
|
+
warnings: list[str] = field(default_factory=list)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def env_var_to_secret_name(env_var: str) -> str:
|
|
97
|
+
"""Return the keyring entry name for an environment variable.
|
|
98
|
+
|
|
99
|
+
The keychain is keyed by the lowercased env var name, which is what
|
|
100
|
+
``API_KEY_NAMES`` and :func:`novacode_cli.onboarding.load_secrets_into_env`
|
|
101
|
+
already use (``OPENAI_API_KEY`` -> ``openai_api_key``).
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
env_var: The environment variable name (e.g. ``OPENAI_API_KEY``).
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The secret name to file the value under.
|
|
108
|
+
"""
|
|
109
|
+
return env_var.lower()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class CredentialStore:
|
|
113
|
+
"""Credential reads and writes bound to one secret manager.
|
|
114
|
+
|
|
115
|
+
Binding matters for cost, not just style: every status or availability pass
|
|
116
|
+
covers the whole provider registry, and a fresh ``SecretManager`` per probe
|
|
117
|
+
re-runs keyring backend detection each time. Callers doing a pass construct
|
|
118
|
+
one store and reuse it. ``None`` stands for "no keychain backend available"
|
|
119
|
+
— reads then report nothing stored and writes report a warning, rather than
|
|
120
|
+
raising into a TUI handler.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, manager: SecretManager | None = None) -> None:
|
|
124
|
+
"""Initialize the store.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
manager: An existing secret manager to reuse, or ``None`` to
|
|
128
|
+
obtain one lazily on first use.
|
|
129
|
+
"""
|
|
130
|
+
self._manager = manager
|
|
131
|
+
self._resolved = manager is not None
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def manager(self) -> SecretManager | None:
|
|
135
|
+
"""The secret manager, resolving one on first access when needed."""
|
|
136
|
+
if not self._resolved:
|
|
137
|
+
self._manager = _default_manager()
|
|
138
|
+
self._resolved = True
|
|
139
|
+
return self._manager
|
|
140
|
+
|
|
141
|
+
def get_key(self, env_var: str) -> str | None:
|
|
142
|
+
"""Read a stored secret without touching the process environment.
|
|
143
|
+
|
|
144
|
+
Deliberately does *not* export the value: this is the read used by
|
|
145
|
+
status and availability checks, which must have no side effects, and by
|
|
146
|
+
callers that only want to know whether a credential exists.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
env_var: The environment variable the credential is filed under.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The stored key, or ``None`` when nothing is stored.
|
|
153
|
+
"""
|
|
154
|
+
if self.manager is None:
|
|
155
|
+
return None
|
|
156
|
+
try:
|
|
157
|
+
value = self.manager.get_secret(env_var_to_secret_name(env_var))
|
|
158
|
+
except Exception: # noqa: BLE001 — a broken keyring must not break the caller
|
|
159
|
+
logger.warning("Could not read stored credential for %s", env_var)
|
|
160
|
+
return None
|
|
161
|
+
return value or None
|
|
162
|
+
|
|
163
|
+
def has_key(self, env_var: str) -> bool:
|
|
164
|
+
"""Whether a credential is stored for *env_var*.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
env_var: The environment variable the credential is filed under.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
``True`` when a non-empty secret is stored.
|
|
171
|
+
"""
|
|
172
|
+
return self.get_key(env_var) is not None
|
|
173
|
+
|
|
174
|
+
def set_key(
|
|
175
|
+
self,
|
|
176
|
+
env_var: str,
|
|
177
|
+
key: str,
|
|
178
|
+
*,
|
|
179
|
+
base_url: str | None = None,
|
|
180
|
+
project: str | None = None,
|
|
181
|
+
) -> WriteOutcome:
|
|
182
|
+
"""Store a credential and pair its endpoint/project with it.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
env_var: The environment variable the credential is filed under.
|
|
186
|
+
key: The secret value as entered by the user.
|
|
187
|
+
base_url: Endpoint to pair with the key, or ``None`` for the
|
|
188
|
+
provider default. Passing ``None`` clears a previously stored
|
|
189
|
+
endpoint, so the key is never left pointing at an endpoint the
|
|
190
|
+
user just removed.
|
|
191
|
+
project: Optional project/workspace name to pair with the key.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
A :class:`WriteOutcome`; ``ok`` is ``False`` only when the secret
|
|
195
|
+
itself could not be stored.
|
|
196
|
+
"""
|
|
197
|
+
if not key.strip():
|
|
198
|
+
return WriteOutcome(ok=False, warnings=["API key cannot be empty."])
|
|
199
|
+
|
|
200
|
+
outcome = WriteOutcome(ok=True)
|
|
201
|
+
if self.manager is None:
|
|
202
|
+
outcome.ok = False
|
|
203
|
+
outcome.warnings.append(
|
|
204
|
+
"No credential store is available on this system; the key was not saved."
|
|
205
|
+
)
|
|
206
|
+
return outcome
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
stored = self.manager.store_secret(env_var_to_secret_name(env_var), key)
|
|
210
|
+
except Exception: # noqa: BLE001 — report, never raise into a UI handler
|
|
211
|
+
logger.warning("Could not write credential for %s", env_var)
|
|
212
|
+
stored = False
|
|
213
|
+
if not stored:
|
|
214
|
+
outcome.ok = False
|
|
215
|
+
outcome.warnings.append(
|
|
216
|
+
f"Could not save the credential for {env_var}; "
|
|
217
|
+
"the OS credential store rejected the write."
|
|
218
|
+
)
|
|
219
|
+
return outcome
|
|
220
|
+
|
|
221
|
+
self._write_meta(env_var, base_url=base_url, project=project, warnings=outcome.warnings)
|
|
222
|
+
# Applied here rather than left to the caller: model construction reads
|
|
223
|
+
# `os.environ`, and a credential that reached the store but not the
|
|
224
|
+
# environment would look saved and still fail until a restart. The
|
|
225
|
+
# metadata is written first because the export reads the paired endpoint.
|
|
226
|
+
try:
|
|
227
|
+
self.apply_to_env(env_var)
|
|
228
|
+
except Exception: # noqa: BLE001 — the key is stored; the export is best-effort
|
|
229
|
+
logger.warning("Could not apply credential for %s to the environment", env_var)
|
|
230
|
+
outcome.warnings.append(
|
|
231
|
+
f"Saved {env_var}, but it could not be applied to this session. "
|
|
232
|
+
"Restart Nova to use it."
|
|
233
|
+
)
|
|
234
|
+
return outcome
|
|
235
|
+
|
|
236
|
+
def delete(self, env_var: str) -> bool:
|
|
237
|
+
"""Delete a stored credential, its metadata, and its env vars.
|
|
238
|
+
|
|
239
|
+
The environment entries are removed too: the running process would
|
|
240
|
+
otherwise keep using a key the user just deleted, and a leftover
|
|
241
|
+
endpoint would keep pointing the provider at it.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
env_var: The environment variable the credential is filed under.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
``True`` when the key is gone (including when it was never stored).
|
|
248
|
+
"""
|
|
249
|
+
removed = True
|
|
250
|
+
if self.manager is not None:
|
|
251
|
+
try:
|
|
252
|
+
removed = bool(self.manager.delete_secret(env_var_to_secret_name(env_var)))
|
|
253
|
+
except Exception: # noqa: BLE001 — report, never raise into a UI handler
|
|
254
|
+
logger.warning("Could not delete credential for %s", env_var)
|
|
255
|
+
removed = False
|
|
256
|
+
self._delete_meta(env_var)
|
|
257
|
+
_set_live_key(env_var, None)
|
|
258
|
+
endpoint_var = _base_url_env_var(env_var)
|
|
259
|
+
if endpoint_var is not None:
|
|
260
|
+
os.environ.pop(endpoint_var, None)
|
|
261
|
+
return removed
|
|
262
|
+
|
|
263
|
+
def meta(self, env_var: str) -> CredentialMeta | None:
|
|
264
|
+
"""Return stored metadata for *env_var*, if any.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
env_var: The environment variable the credential is filed under.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
The metadata, or ``None`` when nothing was recorded.
|
|
271
|
+
"""
|
|
272
|
+
return self.list_meta([env_var]).get(env_var)
|
|
273
|
+
|
|
274
|
+
def list_meta(self, env_vars: Iterable[str]) -> dict[str, CredentialMeta]:
|
|
275
|
+
"""Return metadata for every named credential that has some.
|
|
276
|
+
|
|
277
|
+
Only credentials that recorded something (an endpoint, a project) are
|
|
278
|
+
returned: a plain key writes no metadata, so it never reaches disk.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
env_vars: Candidate environment variable names to look up.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Metadata keyed by environment variable name.
|
|
285
|
+
"""
|
|
286
|
+
from novacode_cli.config.nova_config import NovaConfig
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
recorded = NovaConfig().get_credential_meta()
|
|
290
|
+
except Exception: # noqa: BLE001 — a corrupt config must not break a status pass
|
|
291
|
+
logger.warning("Could not read credential metadata")
|
|
292
|
+
return {}
|
|
293
|
+
|
|
294
|
+
result: dict[str, CredentialMeta] = {}
|
|
295
|
+
for env_var in env_vars:
|
|
296
|
+
raw = recorded.get(env_var)
|
|
297
|
+
if not isinstance(raw, dict):
|
|
298
|
+
continue
|
|
299
|
+
result[env_var] = CredentialMeta(
|
|
300
|
+
env_var=env_var,
|
|
301
|
+
base_url=_clean(raw.get("base_url")) or None,
|
|
302
|
+
project=_clean(raw.get("project")) or None,
|
|
303
|
+
added_at=_clean(raw.get("added_at")) or None,
|
|
304
|
+
)
|
|
305
|
+
return result
|
|
306
|
+
|
|
307
|
+
def apply_to_env(self, env_var: str) -> str | None:
|
|
308
|
+
"""Export a stored credential into the process environment.
|
|
309
|
+
|
|
310
|
+
Model construction reads keys from ``os.environ``, and the base URL env
|
|
311
|
+
var is set from the paired endpoint when one was stored — so a rotation
|
|
312
|
+
that removed the endpoint also clears the exported URL instead of
|
|
313
|
+
leaving the old one in effect.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
env_var: The environment variable the credential is filed under.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
The exported key, or ``None`` when nothing is stored.
|
|
320
|
+
"""
|
|
321
|
+
key = self.get_key(env_var)
|
|
322
|
+
if not key:
|
|
323
|
+
return None
|
|
324
|
+
_set_live_key(env_var, key)
|
|
325
|
+
meta = self.meta(env_var)
|
|
326
|
+
endpoint_var = _base_url_env_var(env_var)
|
|
327
|
+
if endpoint_var is not None:
|
|
328
|
+
if meta is not None and meta.base_url:
|
|
329
|
+
os.environ[endpoint_var] = meta.base_url
|
|
330
|
+
else:
|
|
331
|
+
os.environ.pop(endpoint_var, None)
|
|
332
|
+
return key
|
|
333
|
+
|
|
334
|
+
def _write_meta(
|
|
335
|
+
self,
|
|
336
|
+
env_var: str,
|
|
337
|
+
*,
|
|
338
|
+
base_url: str | None,
|
|
339
|
+
project: str | None,
|
|
340
|
+
warnings: list[str],
|
|
341
|
+
) -> None:
|
|
342
|
+
"""Persist metadata, or drop the record when there is none to keep."""
|
|
343
|
+
from novacode_cli.config.nova_config import NovaConfig
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
config = NovaConfig()
|
|
347
|
+
if base_url or project:
|
|
348
|
+
config.set_credential_meta(
|
|
349
|
+
env_var,
|
|
350
|
+
base_url=base_url,
|
|
351
|
+
project=project,
|
|
352
|
+
added_at=datetime.now(UTC).isoformat(timespec="seconds"),
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
config.delete_credential_meta(env_var)
|
|
356
|
+
except Exception: # noqa: BLE001 — a lost endpoint is not a lost credential
|
|
357
|
+
logger.warning("Could not record credential metadata for %s", env_var)
|
|
358
|
+
warnings.append(
|
|
359
|
+
f"Saved the credential for {env_var}, but its endpoint could not be recorded."
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
@staticmethod
|
|
363
|
+
def _delete_meta(env_var: str) -> None:
|
|
364
|
+
"""Drop metadata for *env_var*, ignoring an unreadable config."""
|
|
365
|
+
from novacode_cli.config.nova_config import NovaConfig
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
NovaConfig().delete_credential_meta(env_var)
|
|
369
|
+
except Exception: # noqa: BLE001 — deletion of the key already succeeded
|
|
370
|
+
logger.warning("Could not clear credential metadata for %s", env_var)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
#: Endpoint env var per credential env var, for providers that accept one.
|
|
374
|
+
#:
|
|
375
|
+
#: Only OpenAI and Azure take an endpoint in Nova (see
|
|
376
|
+
#: :data:`novacode_cli.config.model_manager.ENDPOINT_PROVIDERS`); the gateways
|
|
377
|
+
#: pin their own base URL and Anthropic/Google/Ollama take none at all.
|
|
378
|
+
_BASE_URL_ENV_VARS: dict[str, str] = {
|
|
379
|
+
"OPENAI_API_KEY": "OPENAI_BASE_URL",
|
|
380
|
+
"AZURE_OPENAI_API_KEY": "AZURE_OPENAI_ENDPOINT",
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _base_url_env_var(env_var: str) -> str | None:
|
|
385
|
+
"""Return the endpoint env var paired with a credential env var."""
|
|
386
|
+
return _BASE_URL_ENV_VARS.get(env_var)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _clean(value: object) -> str:
|
|
390
|
+
"""Normalize a recorded metadata field to a stripped string."""
|
|
391
|
+
return value.strip() if isinstance(value, str) else ""
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _default_manager() -> SecretManager | None:
|
|
395
|
+
"""Build a secret manager, or ``None`` when no backend is usable.
|
|
396
|
+
|
|
397
|
+
Imported here rather than at module scope for two reasons: ``onboarding``
|
|
398
|
+
imports back into ``config``, and tests replace
|
|
399
|
+
``novacode_cli.onboarding.SecretManager`` — a module-scope binding would
|
|
400
|
+
capture the real class before that patch lands.
|
|
401
|
+
"""
|
|
402
|
+
try:
|
|
403
|
+
from novacode_cli.onboarding import SecretManager
|
|
404
|
+
except Exception: # noqa: BLE001 — a missing keyring is not a hard failure
|
|
405
|
+
logger.debug("Secret storage is unavailable; credentials cannot be read")
|
|
406
|
+
return None
|
|
407
|
+
try:
|
|
408
|
+
return SecretManager()
|
|
409
|
+
except Exception: # noqa: BLE001 — a broken keyring backend must degrade, not crash
|
|
410
|
+
logger.warning("OS credential store is unavailable; using no credential store")
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def default_store() -> CredentialStore:
|
|
415
|
+
"""Return a credential store over the default secret manager."""
|
|
416
|
+
return CredentialStore()
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# ---------------------------------------------------------------------------
|
|
420
|
+
# Convenience wrappers — one secret manager per call
|
|
421
|
+
#
|
|
422
|
+
# For a single lookup. A pass over the whole registry should build one
|
|
423
|
+
# `CredentialStore` and reuse it (see `provider_auth`).
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def get_stored_key(env_var: str) -> str | None:
|
|
428
|
+
"""Read a stored credential without exporting it (see `get_key`)."""
|
|
429
|
+
return default_store().get_key(env_var)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def has_stored_credential(env_var: str) -> bool:
|
|
433
|
+
"""Whether a credential is stored for *env_var*."""
|
|
434
|
+
return default_store().has_key(env_var)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def set_credential(
|
|
438
|
+
env_var: str,
|
|
439
|
+
key: str,
|
|
440
|
+
*,
|
|
441
|
+
base_url: str | None = None,
|
|
442
|
+
project: str | None = None,
|
|
443
|
+
) -> WriteOutcome:
|
|
444
|
+
"""Store a credential and pair its endpoint with it."""
|
|
445
|
+
return default_store().set_key(env_var, key, base_url=base_url, project=project)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def delete_credential(env_var: str) -> bool:
|
|
449
|
+
"""Delete a stored credential, its metadata, and its env var."""
|
|
450
|
+
return default_store().delete(env_var)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def credential_meta(env_var: str) -> CredentialMeta | None:
|
|
454
|
+
"""Return stored metadata for *env_var*, if any."""
|
|
455
|
+
return default_store().meta(env_var)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def apply_credential_to_env(env_var: str) -> str | None:
|
|
459
|
+
"""Export a stored credential into the process environment."""
|
|
460
|
+
return default_store().apply_to_env(env_var)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def credential_value(env_var: str) -> str:
|
|
464
|
+
"""The stored credential for *env_var*, else the environment, else ``""``.
|
|
465
|
+
|
|
466
|
+
The order is the point. A key entered through the UI lives in the keychain,
|
|
467
|
+
and only the startup hydration pass puts it in ``os.environ``, so a caller
|
|
468
|
+
reading the environment alone sees nothing until the process restarts. This
|
|
469
|
+
is the lookup for code that was written against ``os.environ`` directly.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
env_var: The environment variable the credential is filed under.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
The key, or an empty string when none is available anywhere.
|
|
476
|
+
"""
|
|
477
|
+
stored = get_stored_key(env_var)
|
|
478
|
+
if stored:
|
|
479
|
+
return stored
|
|
480
|
+
return os.environ.get(env_var) or ""
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
#: Cloud voice providers, mapped to their credential env var and the config
|
|
484
|
+
#: provider id those keys used to be written to. Both lived in plaintext in
|
|
485
|
+
#: ``Nova.config.json`` under ``voice.providers.<id>``.
|
|
486
|
+
_LEGACY_VOICE_KEYS: dict[str, str] = {
|
|
487
|
+
"DEEPGRAM_API_KEY": "deepgram",
|
|
488
|
+
"ELEVENLABS_API_KEY": "elevenlabs",
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def migrate_voice_keys() -> list[str]:
|
|
493
|
+
"""Move plaintext voice keys from the config file into the keychain.
|
|
494
|
+
|
|
495
|
+
Earlier releases stored these under ``voice.providers.<id>.api_key`` (and a
|
|
496
|
+
legacy ``.key`` duplicate), because the voice backends read their key from
|
|
497
|
+
the config rather than the environment. The value is copied to the keychain
|
|
498
|
+
and the config field is blanked, so the file stops carrying a secret.
|
|
499
|
+
|
|
500
|
+
Idempotent, and safe to call on every startup: a provider with no plaintext
|
|
501
|
+
key is skipped, and one whose key is already stored only has the redundant
|
|
502
|
+
copy cleared. Writes to the config are non-fatal — a failure leaves the
|
|
503
|
+
plaintext in place, which the pipeline still honors via its fallback chain.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
The env var names whose plaintext copy was removed, for reporting.
|
|
507
|
+
"""
|
|
508
|
+
from novacode_cli.config.nova_config import NovaConfig
|
|
509
|
+
|
|
510
|
+
migrated: list[str] = []
|
|
511
|
+
try:
|
|
512
|
+
config = NovaConfig()
|
|
513
|
+
store = default_store()
|
|
514
|
+
except Exception: # noqa: BLE001 — startup must not fail on a config problem
|
|
515
|
+
logger.warning("Could not open the config for voice key migration")
|
|
516
|
+
return migrated
|
|
517
|
+
|
|
518
|
+
for env_var, provider in _LEGACY_VOICE_KEYS.items():
|
|
519
|
+
try:
|
|
520
|
+
provider_cfg = config.get_voice_provider_config(provider)
|
|
521
|
+
except Exception: # noqa: BLE001
|
|
522
|
+
logger.warning("Could not read voice config for %s", provider)
|
|
523
|
+
continue
|
|
524
|
+
plaintext = _clean(provider_cfg.get("api_key")) or _clean(provider_cfg.get("key"))
|
|
525
|
+
if not plaintext:
|
|
526
|
+
continue
|
|
527
|
+
try:
|
|
528
|
+
if not store.has_key(env_var):
|
|
529
|
+
outcome = store.set_key(env_var, plaintext)
|
|
530
|
+
if not outcome.ok:
|
|
531
|
+
logger.warning("Could not store the %s key", env_var)
|
|
532
|
+
continue
|
|
533
|
+
# Blank both fields: `api_key` and the legacy `key` alias the older
|
|
534
|
+
# `/voice settings --key` wrote.
|
|
535
|
+
config.set_voice_provider_config(provider, api_key="", key="")
|
|
536
|
+
except Exception: # noqa: BLE001
|
|
537
|
+
logger.warning("Could not move the %s key out of the config", env_var)
|
|
538
|
+
continue
|
|
539
|
+
migrated.append(env_var)
|
|
540
|
+
return migrated
|