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,168 @@
|
|
|
1
|
+
"""One plugin management command service for the shell and Nova interfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import NoReturn
|
|
10
|
+
|
|
11
|
+
from novacode_cli.plugins import claude_plugins as cp
|
|
12
|
+
from novacode_cli.plugins import marketplaces as mp
|
|
13
|
+
from novacode_cli.plugins.components import Plugin
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _HelpRequestedError(Exception):
|
|
17
|
+
"""Carry formatted help to the interface instead of printing into the TUI."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Parser(argparse.ArgumentParser):
|
|
21
|
+
"""Report argument errors to the host without terminating the TUI."""
|
|
22
|
+
|
|
23
|
+
def error(self, message: str) -> NoReturn:
|
|
24
|
+
"""Return parser errors to the calling interface."""
|
|
25
|
+
raise ValueError(message)
|
|
26
|
+
|
|
27
|
+
def print_help(self, file: object = None) -> None: # noqa: ARG002 -- argparse interface.
|
|
28
|
+
"""Return help through the management result, including subcommand help."""
|
|
29
|
+
raise _HelpRequestedError(self.format_help())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Result:
|
|
34
|
+
"""The UI chooses a report view or a temporary operation notification."""
|
|
35
|
+
|
|
36
|
+
lines: tuple[str, ...]
|
|
37
|
+
changed: bool = False
|
|
38
|
+
report: bool = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parser() -> Parser:
|
|
42
|
+
"""Build the shared shell/slash-command grammar."""
|
|
43
|
+
root = Parser(prog="nova plugin", description="Manage standard directory plugins")
|
|
44
|
+
commands = root.add_subparsers(dest="action")
|
|
45
|
+
for action in ("install", "uninstall", "remove", "enable", "disable", "update"):
|
|
46
|
+
command = commands.add_parser(action)
|
|
47
|
+
command.add_argument("plugin")
|
|
48
|
+
command.add_argument("--scope", "-s", choices=("user", "project", "local"))
|
|
49
|
+
for action in ("list", "search"):
|
|
50
|
+
command = commands.add_parser(action)
|
|
51
|
+
command.add_argument("query", nargs="?", default="")
|
|
52
|
+
validate = commands.add_parser("validate")
|
|
53
|
+
validate.add_argument("path")
|
|
54
|
+
marketplace = commands.add_parser("marketplace")
|
|
55
|
+
marketplace.add_argument("operation", choices=("add", "remove", "list", "update"))
|
|
56
|
+
marketplace.add_argument("source", nargs="?", default="")
|
|
57
|
+
return root
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def execute(arguments: list[str]) -> Result: # noqa: PLR0911, PLR0912 -- one branch per owner command.
|
|
61
|
+
"""Execute owner-requested management; install never runs plugin components."""
|
|
62
|
+
try:
|
|
63
|
+
args = parser().parse_args(arguments)
|
|
64
|
+
except _HelpRequestedError as help_requested:
|
|
65
|
+
return Result((str(help_requested),), report=True)
|
|
66
|
+
action = args.action or "list"
|
|
67
|
+
if action == "install":
|
|
68
|
+
source = args.plugin
|
|
69
|
+
if ("@" in source and not source.startswith(("git@", "ssh://"))) or (
|
|
70
|
+
not Path(source).expanduser().is_dir() and "/" not in source and ":" not in source
|
|
71
|
+
):
|
|
72
|
+
name = mp.install_plugin(source, scope=args.scope or "user")
|
|
73
|
+
else:
|
|
74
|
+
name = cp.install(source, scope=args.scope or "user")
|
|
75
|
+
warnings = cp.plugin_components(name).get("diagnostics", [])
|
|
76
|
+
return Result(
|
|
77
|
+
(f"Installed {name}. /reload-plugins applies it to this session.", *warnings),
|
|
78
|
+
changed=True,
|
|
79
|
+
)
|
|
80
|
+
if action in {"uninstall", "remove"}:
|
|
81
|
+
removed = cp.remove(args.plugin, scope=args.scope or "user")
|
|
82
|
+
return Result(
|
|
83
|
+
(
|
|
84
|
+
f"Uninstalled {args.plugin}."
|
|
85
|
+
if removed
|
|
86
|
+
else "No matching installation at that scope.",
|
|
87
|
+
),
|
|
88
|
+
changed=removed,
|
|
89
|
+
)
|
|
90
|
+
if action in {"enable", "disable"}:
|
|
91
|
+
cp.set_enabled(args.plugin, enabled=action == "enable", scope=args.scope)
|
|
92
|
+
return Result(
|
|
93
|
+
(f"{action.title()}d {args.plugin}. /reload-plugins applies this change.",),
|
|
94
|
+
changed=True,
|
|
95
|
+
)
|
|
96
|
+
if action == "update":
|
|
97
|
+
name = cp.update(args.plugin, scope=args.scope)
|
|
98
|
+
return Result((f"Updated {name}. /reload-plugins loads the new snapshot.",), changed=True)
|
|
99
|
+
if action == "validate":
|
|
100
|
+
directory = Path(args.path).expanduser().resolve()
|
|
101
|
+
if (directory / ".claude-plugin/marketplace.json").is_file():
|
|
102
|
+
mp._marketplace_json(directory)
|
|
103
|
+
warnings = []
|
|
104
|
+
else:
|
|
105
|
+
warnings = Plugin(directory, directory.name).validate()
|
|
106
|
+
return Result(
|
|
107
|
+
(
|
|
108
|
+
"Validation passed."
|
|
109
|
+
if not warnings
|
|
110
|
+
else "Validation passed with compatibility warnings.",
|
|
111
|
+
*warnings,
|
|
112
|
+
),
|
|
113
|
+
report=True,
|
|
114
|
+
)
|
|
115
|
+
if action == "marketplace":
|
|
116
|
+
operation, source = args.operation, args.source
|
|
117
|
+
if operation == "list":
|
|
118
|
+
return Result(
|
|
119
|
+
tuple(f"{m['name']}: {m['source']}" for m in mp.list_marketplaces())
|
|
120
|
+
or ("No marketplaces.",),
|
|
121
|
+
report=True,
|
|
122
|
+
)
|
|
123
|
+
if not source:
|
|
124
|
+
msg = "Marketplace operation requires a source or name"
|
|
125
|
+
raise ValueError(msg)
|
|
126
|
+
if operation == "add":
|
|
127
|
+
name = mp.add(source)
|
|
128
|
+
return Result((f"Added marketplace {name}.",), changed=True)
|
|
129
|
+
if operation == "remove":
|
|
130
|
+
removed = mp.remove_marketplace(source)
|
|
131
|
+
return Result(
|
|
132
|
+
("Marketplace removed." if removed else "Marketplace not found.",), changed=removed
|
|
133
|
+
)
|
|
134
|
+
name = mp.update_marketplace(source)
|
|
135
|
+
return Result((f"Updated marketplace {name}.",), changed=True)
|
|
136
|
+
if action == "search":
|
|
137
|
+
query = args.query.lower()
|
|
138
|
+
matches = [
|
|
139
|
+
p
|
|
140
|
+
for p in mp.list_marketplace_plugins()
|
|
141
|
+
if query in (p["name"] + " " + p["description"]).lower()
|
|
142
|
+
]
|
|
143
|
+
return Result(
|
|
144
|
+
tuple(f"{p['name']}@{p['marketplace']}: {p['description']}" for p in matches)
|
|
145
|
+
or ("No matching plugins.",),
|
|
146
|
+
report=True,
|
|
147
|
+
)
|
|
148
|
+
return Result(
|
|
149
|
+
tuple(
|
|
150
|
+
f"{p['id']} ({p['scope']}, {'enabled' if p['enabled'] else 'disabled'}) "
|
|
151
|
+
f"{p.get('version', 'unversioned')}"
|
|
152
|
+
for p in cp.list_plugins()
|
|
153
|
+
)
|
|
154
|
+
or ("No plugins installed.",),
|
|
155
|
+
report=True,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main(arguments: list[str]) -> int:
|
|
160
|
+
"""Run without an agent or provider; use the same ownership and scope rules."""
|
|
161
|
+
try:
|
|
162
|
+
result = execute(arguments)
|
|
163
|
+
for line in result.lines:
|
|
164
|
+
sys.stdout.write(line + "\n")
|
|
165
|
+
except (OSError, ValueError, RuntimeError, KeyError) as exc:
|
|
166
|
+
sys.stderr.write(f"Plugin operation failed: {exc}\n")
|
|
167
|
+
return 1
|
|
168
|
+
return 0
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Read standard plugin components without executing or importing plugin code."""
|
|
2
|
+
|
|
3
|
+
# ruff: noqa: ANN401, TRY004 -- JSON schema errors use one ValueError contract.
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path, PureWindowsPath
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
MAX_HOOK_TIMEOUT = 60
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def object_file(path: Path, *, optional: bool = False) -> dict[str, Any]:
|
|
18
|
+
"""Read a JSON object; malformed files must never become empty configuration."""
|
|
19
|
+
try:
|
|
20
|
+
data = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
21
|
+
except FileNotFoundError:
|
|
22
|
+
if optional:
|
|
23
|
+
return {}
|
|
24
|
+
raise
|
|
25
|
+
except (UnicodeError, json.JSONDecodeError) as exc:
|
|
26
|
+
msg = f"Invalid JSON in {path.name}: {exc}"
|
|
27
|
+
raise ValueError(msg) from exc
|
|
28
|
+
if not isinstance(data, dict):
|
|
29
|
+
msg = f"Expected an object in {path.name}"
|
|
30
|
+
raise ValueError(msg)
|
|
31
|
+
return data
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def safe_name(name: str) -> str:
|
|
35
|
+
"""Reject ambiguous IDs, traversal and Windows device destinations."""
|
|
36
|
+
if (
|
|
37
|
+
not isinstance(name, str)
|
|
38
|
+
or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}", name)
|
|
39
|
+
or ".." in name
|
|
40
|
+
or name.endswith(".")
|
|
41
|
+
or re.match(r"^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)", name, re.IGNORECASE)
|
|
42
|
+
or name in {"installed.json", "marketplaces.json", "cache"}
|
|
43
|
+
):
|
|
44
|
+
msg = "Invalid plugin or marketplace name"
|
|
45
|
+
raise ValueError(msg)
|
|
46
|
+
return name
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def contained(root: Path, value: str, *, exists: bool = True) -> Path:
|
|
50
|
+
"""Resolve only relative paths that remain inside their owning bundle."""
|
|
51
|
+
if not isinstance(value, str) or Path(value).is_absolute() or PureWindowsPath(value).drive:
|
|
52
|
+
msg = "Component path must be relative to the plugin root"
|
|
53
|
+
raise ValueError(msg)
|
|
54
|
+
if ".." in value.replace("\\", "/").split("/") or value.startswith("\\"):
|
|
55
|
+
msg = "Component path escapes plugin root"
|
|
56
|
+
raise ValueError(msg)
|
|
57
|
+
path = root / value
|
|
58
|
+
if not path.resolve().is_relative_to(root.resolve()):
|
|
59
|
+
msg = "Component path escapes plugin root through a link"
|
|
60
|
+
raise ValueError(msg)
|
|
61
|
+
if exists and not path.exists():
|
|
62
|
+
msg = f"Missing plugin component: {value}"
|
|
63
|
+
raise ValueError(msg)
|
|
64
|
+
return path
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def frontmatter(text: str) -> tuple[dict[str, Any], str]:
|
|
68
|
+
"""Parse real YAML metadata, including block descriptions and tool lists."""
|
|
69
|
+
match = re.match(r"\A---\s*\n(.*?)\n---(?:\n|$)", text, re.DOTALL)
|
|
70
|
+
if not match:
|
|
71
|
+
return {}, text
|
|
72
|
+
try:
|
|
73
|
+
data = yaml.safe_load(match[1]) or {}
|
|
74
|
+
except yaml.YAMLError as exc:
|
|
75
|
+
message = "Invalid YAML in plugin frontmatter"
|
|
76
|
+
raise ValueError(message) from exc
|
|
77
|
+
if not isinstance(data, dict):
|
|
78
|
+
msg = "Plugin frontmatter must be a YAML object"
|
|
79
|
+
raise ValueError(msg)
|
|
80
|
+
return data, text[match.end() :].lstrip("\n")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Plugin:
|
|
84
|
+
"""One validated bundle and its standard/manifest component locations."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, root: Path, fallback: str, entry: dict[str, Any] | None = None) -> None:
|
|
87
|
+
"""Open a bundle after checking its manifest and root boundaries."""
|
|
88
|
+
self.root = root.resolve()
|
|
89
|
+
if not self.root.is_dir():
|
|
90
|
+
message = "Plugin directory does not exist"
|
|
91
|
+
raise ValueError(message)
|
|
92
|
+
if (root / ".claude-plugin/plugin.json").exists():
|
|
93
|
+
contained(root, ".claude-plugin/plugin.json")
|
|
94
|
+
self.manifest = object_file(root / ".claude-plugin/plugin.json", optional=True)
|
|
95
|
+
if (root / ".claude-plugin/plugin.json").exists() and "name" not in self.manifest:
|
|
96
|
+
msg = "A plugin manifest must declare its name"
|
|
97
|
+
raise ValueError(msg)
|
|
98
|
+
self.name = safe_name(self.manifest.get("name", fallback))
|
|
99
|
+
self.entry = entry or {}
|
|
100
|
+
self._merge_entry()
|
|
101
|
+
for key in ("version", "description", "displayName"):
|
|
102
|
+
if key in self.manifest and not isinstance(self.manifest[key], str):
|
|
103
|
+
msg = f"Plugin {key} must be a string"
|
|
104
|
+
raise ValueError(msg)
|
|
105
|
+
if "defaultEnabled" in self.manifest and not isinstance(
|
|
106
|
+
self.manifest["defaultEnabled"], bool
|
|
107
|
+
):
|
|
108
|
+
msg = "defaultEnabled must be boolean"
|
|
109
|
+
raise ValueError(msg)
|
|
110
|
+
|
|
111
|
+
def _merge_entry(self) -> None: # noqa: PLR0912 -- documented precedence for each component kind.
|
|
112
|
+
"""Combine catalog declarations using the documented strict/metadata rules."""
|
|
113
|
+
if not self.entry:
|
|
114
|
+
return
|
|
115
|
+
component_keys = ("commands", "agents", "skills", "hooks", "outputStyles", "themes")
|
|
116
|
+
has_manifest = (self.root / ".claude-plugin/plugin.json").is_file()
|
|
117
|
+
if (
|
|
118
|
+
has_manifest
|
|
119
|
+
and self.entry.get("strict", True) is False
|
|
120
|
+
and any(key in self.entry for key in component_keys)
|
|
121
|
+
):
|
|
122
|
+
message = "Plugin has conflicting manifests (strict: false)"
|
|
123
|
+
raise ValueError(message)
|
|
124
|
+
if not has_manifest:
|
|
125
|
+
self.manifest = dict(self.entry)
|
|
126
|
+
return
|
|
127
|
+
manifest = dict(self.manifest)
|
|
128
|
+
for key in ("defaultEnabled", "displayName"):
|
|
129
|
+
if key in self.entry:
|
|
130
|
+
manifest[key] = self.entry[key]
|
|
131
|
+
for key in ("version", "description"):
|
|
132
|
+
if key not in manifest and key in self.entry:
|
|
133
|
+
manifest[key] = self.entry[key]
|
|
134
|
+
for key in ("commands", "agents", "skills"):
|
|
135
|
+
if key not in self.entry:
|
|
136
|
+
continue
|
|
137
|
+
left = manifest.get(key, [])
|
|
138
|
+
right = self.entry[key]
|
|
139
|
+
if isinstance(left, dict) or isinstance(right, dict):
|
|
140
|
+
if (left and not isinstance(left, dict)) or not isinstance(right, dict):
|
|
141
|
+
message = "Cannot combine path and inline command declarations"
|
|
142
|
+
raise ValueError(message)
|
|
143
|
+
manifest[key] = {**(left or {}), **right}
|
|
144
|
+
continue
|
|
145
|
+
left = [left] if isinstance(left, str) else left
|
|
146
|
+
right = [right] if isinstance(right, str) else right
|
|
147
|
+
if not isinstance(left, list) or not isinstance(right, list):
|
|
148
|
+
message = "Catalog component paths must be strings or lists"
|
|
149
|
+
raise ValueError(message)
|
|
150
|
+
default = self.root / key
|
|
151
|
+
if key not in manifest and default.is_dir() and key != "skills":
|
|
152
|
+
left = ["./" + key]
|
|
153
|
+
manifest[key] = [*left, *right]
|
|
154
|
+
self.manifest = manifest
|
|
155
|
+
|
|
156
|
+
def paths(self, key: str, default: str, *, additive: bool = False) -> list[Path]:
|
|
157
|
+
"""Resolve explicit component paths, with each component's merge rules."""
|
|
158
|
+
values = self.manifest.get(key, [])
|
|
159
|
+
if isinstance(values, str):
|
|
160
|
+
values = [values]
|
|
161
|
+
if not isinstance(values, list) or any(not isinstance(v, str) for v in values):
|
|
162
|
+
msg = f"{key} must be a path or list of paths"
|
|
163
|
+
raise ValueError(msg)
|
|
164
|
+
result = [contained(self.root, value) for value in values]
|
|
165
|
+
default_path = self.root / default
|
|
166
|
+
if (additive or key not in self.manifest) and default_path.exists():
|
|
167
|
+
contained(self.root, default)
|
|
168
|
+
result.insert(0, default_path)
|
|
169
|
+
return list(dict.fromkeys(result))
|
|
170
|
+
|
|
171
|
+
def skills(self) -> list[Path]:
|
|
172
|
+
"""Find skill folders; explicit skill directories supplement defaults."""
|
|
173
|
+
folders = []
|
|
174
|
+
entry_only = (
|
|
175
|
+
self.entry.get("source") in {".", "./"}
|
|
176
|
+
if isinstance(self.entry.get("source"), str)
|
|
177
|
+
else False
|
|
178
|
+
)
|
|
179
|
+
for path in self.paths(
|
|
180
|
+
"skills", "skills", additive=not (entry_only and "skills" in self.entry)
|
|
181
|
+
):
|
|
182
|
+
candidates = [path] if (path / "SKILL.md").is_file() else sorted(path.iterdir())
|
|
183
|
+
for candidate in candidates:
|
|
184
|
+
if (candidate / "SKILL.md").is_file():
|
|
185
|
+
contained(self.root, str(candidate.relative_to(self.root)))
|
|
186
|
+
contained(self.root, str((candidate / "SKILL.md").relative_to(self.root)))
|
|
187
|
+
folders.append(candidate)
|
|
188
|
+
return list(dict.fromkeys(folders))
|
|
189
|
+
|
|
190
|
+
def commands(self) -> list[tuple[str, str, str]]: # noqa: PLR0912 -- standard manifest shapes and legacy TOML support.
|
|
191
|
+
"""Load namespaced legacy commands and skills as callable commands."""
|
|
192
|
+
result = {}
|
|
193
|
+
declared = self.manifest.get("commands")
|
|
194
|
+
if isinstance(declared, dict):
|
|
195
|
+
for name, entry in declared.items():
|
|
196
|
+
safe_name(name)
|
|
197
|
+
if not isinstance(entry, dict):
|
|
198
|
+
msg = "Inline command must declare source or content"
|
|
199
|
+
raise ValueError(msg)
|
|
200
|
+
if "source" in entry:
|
|
201
|
+
path = contained(self.root, entry["source"])
|
|
202
|
+
fm, body = frontmatter(path.read_text(encoding="utf-8"))
|
|
203
|
+
else:
|
|
204
|
+
fm, body = entry, entry.get("content", "")
|
|
205
|
+
if not isinstance(body, str):
|
|
206
|
+
msg = "Command content must be a string"
|
|
207
|
+
raise ValueError(msg)
|
|
208
|
+
result[name] = (str(fm.get("description", name)), body)
|
|
209
|
+
else:
|
|
210
|
+
for path in self.paths("commands", "commands"):
|
|
211
|
+
files = (
|
|
212
|
+
sorted(path.rglob("*.md")) + sorted(path.rglob("*.toml"))
|
|
213
|
+
if path.is_dir()
|
|
214
|
+
else [path]
|
|
215
|
+
)
|
|
216
|
+
for file in files:
|
|
217
|
+
contained(self.root, str(file.relative_to(self.root)))
|
|
218
|
+
if file.suffix == ".toml":
|
|
219
|
+
import tomllib
|
|
220
|
+
|
|
221
|
+
data = tomllib.loads(file.read_text(encoding="utf-8"))
|
|
222
|
+
fm, body = data, data.get("prompt", "")
|
|
223
|
+
else:
|
|
224
|
+
fm, body = frontmatter(file.read_text(encoding="utf-8"))
|
|
225
|
+
result[file.stem] = (str(fm.get("description", file.stem)), body)
|
|
226
|
+
for folder in self.skills():
|
|
227
|
+
fm, body = frontmatter((folder / "SKILL.md").read_text(encoding="utf-8"))
|
|
228
|
+
if fm.get("user-invocable", True) is not False:
|
|
229
|
+
result[folder.name] = (str(fm.get("description", folder.name)), body)
|
|
230
|
+
return [
|
|
231
|
+
(f"{self.name}:{safe_name(name)}", desc, self.expand(body))
|
|
232
|
+
for name, (desc, body) in result.items()
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
def configs(self, key: str, default: str) -> list[dict[str, Any]]:
|
|
236
|
+
"""Merge default JSON plus inline objects and explicit JSON file paths."""
|
|
237
|
+
result = []
|
|
238
|
+
if (self.root / default).is_file():
|
|
239
|
+
result.append(object_file(contained(self.root, default)))
|
|
240
|
+
values = self.manifest.get(key, [])
|
|
241
|
+
if (
|
|
242
|
+
key == "mcpServers"
|
|
243
|
+
and key in self.entry
|
|
244
|
+
and (self.root / ".claude-plugin/plugin.json").is_file()
|
|
245
|
+
):
|
|
246
|
+
entry_values = self.entry[key]
|
|
247
|
+
values = (values if isinstance(values, list) else [values]) + (
|
|
248
|
+
entry_values if isinstance(entry_values, list) else [entry_values]
|
|
249
|
+
)
|
|
250
|
+
if not isinstance(values, list):
|
|
251
|
+
values = [values]
|
|
252
|
+
for value in values:
|
|
253
|
+
if isinstance(value, dict):
|
|
254
|
+
result.append(value)
|
|
255
|
+
elif isinstance(value, str) and value.endswith(".json"):
|
|
256
|
+
path = contained(self.root, value)
|
|
257
|
+
if path.resolve() != (self.root / default).resolve():
|
|
258
|
+
result.append(object_file(path))
|
|
259
|
+
else:
|
|
260
|
+
msg = f"Unsupported {key} component: expected JSON file or object"
|
|
261
|
+
raise ValueError(msg)
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
def mcp(self) -> dict[str, dict[str, Any]]:
|
|
265
|
+
"""Return server configuration while retaining environment placeholders."""
|
|
266
|
+
out = {}
|
|
267
|
+
for config in self.configs("mcpServers", ".mcp.json"):
|
|
268
|
+
servers = config.get("mcpServers", config)
|
|
269
|
+
if not isinstance(servers, dict):
|
|
270
|
+
msg = "mcpServers must be an object"
|
|
271
|
+
raise ValueError(msg)
|
|
272
|
+
for name, value in servers.items():
|
|
273
|
+
safe_name(name)
|
|
274
|
+
if not isinstance(value, dict):
|
|
275
|
+
msg = "MCP server must be an object"
|
|
276
|
+
raise ValueError(msg)
|
|
277
|
+
out[name] = self.expand(value)
|
|
278
|
+
return out
|
|
279
|
+
|
|
280
|
+
def hook_configs(self) -> list[dict[str, Any]]:
|
|
281
|
+
"""Read standard command hooks without dropping matchers or options."""
|
|
282
|
+
configurations = self.configs("hooks", "hooks/hooks.json")
|
|
283
|
+
if "hooks" not in self.entry:
|
|
284
|
+
return configurations
|
|
285
|
+
if not isinstance(self.entry["hooks"], dict):
|
|
286
|
+
message = "Only inline hooks are supported in marketplace entries"
|
|
287
|
+
raise ValueError(message)
|
|
288
|
+
events: dict[str, list] = {}
|
|
289
|
+
for config in configurations:
|
|
290
|
+
for event, entries in config.get("hooks", {}).items():
|
|
291
|
+
events.setdefault(event, []).extend(entries)
|
|
292
|
+
events.update(self.entry["hooks"].get("hooks", {}))
|
|
293
|
+
return [{"hooks": events}]
|
|
294
|
+
|
|
295
|
+
def agents(self) -> list[dict[str, Any]]:
|
|
296
|
+
"""Read subagent definitions with component names scoped to the plugin."""
|
|
297
|
+
result = []
|
|
298
|
+
for path in self.paths("agents", "agents"):
|
|
299
|
+
for file in sorted(path.glob("*.md")) if path.is_dir() else [path]:
|
|
300
|
+
contained(self.root, str(file.relative_to(self.root)))
|
|
301
|
+
text = file.read_text(encoding="utf-8")
|
|
302
|
+
fm, body = frontmatter(text)
|
|
303
|
+
name = safe_name(fm.get("name", file.stem))
|
|
304
|
+
from novacode_cli.agents.agent_file import ALWAYS_INCLUDED, agent_tools
|
|
305
|
+
|
|
306
|
+
declared_tools = fm.get("tools")
|
|
307
|
+
if declared_tools is not None and not (
|
|
308
|
+
isinstance(declared_tools, str)
|
|
309
|
+
or (
|
|
310
|
+
isinstance(declared_tools, list)
|
|
311
|
+
and all(isinstance(tool, str) for tool in declared_tools)
|
|
312
|
+
)
|
|
313
|
+
):
|
|
314
|
+
message = "Agent tools must be a string or a list of strings"
|
|
315
|
+
raise ValueError(message)
|
|
316
|
+
chosen = agent_tools(text)
|
|
317
|
+
result.append(
|
|
318
|
+
{
|
|
319
|
+
"name": f"{self.name}:{name}",
|
|
320
|
+
"description": str(fm.get("description", name)),
|
|
321
|
+
"system_prompt": self.expand(body),
|
|
322
|
+
"tools": [],
|
|
323
|
+
**(
|
|
324
|
+
{
|
|
325
|
+
"_nova_tool_names": chosen,
|
|
326
|
+
"_nova_fs_allow": set(chosen) & set(ALWAYS_INCLUDED),
|
|
327
|
+
}
|
|
328
|
+
if chosen is not None
|
|
329
|
+
else {}
|
|
330
|
+
),
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
return result
|
|
334
|
+
|
|
335
|
+
def expand(self, value: Any) -> Any:
|
|
336
|
+
"""Replace plugin-root references structurally, never through a shell."""
|
|
337
|
+
if isinstance(value, str):
|
|
338
|
+
return value.replace("${CLAUDE_PLUGIN_ROOT}", str(self.root))
|
|
339
|
+
if isinstance(value, list):
|
|
340
|
+
return [self.expand(v) for v in value]
|
|
341
|
+
if isinstance(value, dict):
|
|
342
|
+
return {k: self.expand(v) for k, v in value.items()}
|
|
343
|
+
return value
|
|
344
|
+
|
|
345
|
+
def validate(self) -> list[str]:
|
|
346
|
+
"""Validate all supported paths and report host-specific components."""
|
|
347
|
+
self.skills(), self.commands(), self.agents(), self.mcp()
|
|
348
|
+
for configuration in self.hook_configs():
|
|
349
|
+
_validate_hooks(configuration)
|
|
350
|
+
unsupported = [
|
|
351
|
+
key
|
|
352
|
+
for key in (
|
|
353
|
+
"lspServers",
|
|
354
|
+
"channels",
|
|
355
|
+
"workflows",
|
|
356
|
+
"outputStyles",
|
|
357
|
+
"userConfig",
|
|
358
|
+
"dependencies",
|
|
359
|
+
)
|
|
360
|
+
if key in self.manifest
|
|
361
|
+
]
|
|
362
|
+
if (self.root / ".lsp.json").exists():
|
|
363
|
+
unsupported.append(".lsp.json")
|
|
364
|
+
if (self.root / "hooks/hooks.js").exists():
|
|
365
|
+
unsupported.append("JavaScript hooks module")
|
|
366
|
+
return [f"Unsupported in Nova: {key}" for key in unsupported]
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _validate_hooks(configuration: dict[str, Any]) -> None: # noqa: PLR0912 -- validate each protocol field before publication.
|
|
370
|
+
"""Reject malformed hook entries before the bundle can be enabled."""
|
|
371
|
+
events = configuration.get("hooks")
|
|
372
|
+
if not isinstance(events, dict):
|
|
373
|
+
msg = "Plugin hooks must contain an events object"
|
|
374
|
+
raise ValueError(msg)
|
|
375
|
+
for entries in events.values():
|
|
376
|
+
if not isinstance(entries, list):
|
|
377
|
+
msg = "Hook event entries must be a list"
|
|
378
|
+
raise ValueError(msg)
|
|
379
|
+
for entry in entries:
|
|
380
|
+
if not isinstance(entry, dict) or not isinstance(entry.get("hooks"), list):
|
|
381
|
+
msg = "Hook entry must contain a hooks list"
|
|
382
|
+
raise ValueError(msg)
|
|
383
|
+
matcher = entry.get("matcher", "")
|
|
384
|
+
if not isinstance(matcher, str):
|
|
385
|
+
msg = "Hook matcher must be a string"
|
|
386
|
+
raise ValueError(msg)
|
|
387
|
+
if matcher not in {"", "*"}:
|
|
388
|
+
try:
|
|
389
|
+
re.compile(matcher)
|
|
390
|
+
except re.error as exc:
|
|
391
|
+
msg = "Invalid plugin hook matcher"
|
|
392
|
+
raise ValueError(msg) from exc
|
|
393
|
+
for hook in entry["hooks"]:
|
|
394
|
+
if not isinstance(hook, dict) or hook.get("type") != "command":
|
|
395
|
+
msg = "Nova currently supports command hooks only"
|
|
396
|
+
raise ValueError(msg)
|
|
397
|
+
if not isinstance(hook.get("command"), str):
|
|
398
|
+
msg = "Hook command must be a string"
|
|
399
|
+
raise ValueError(msg)
|
|
400
|
+
if "args" in hook and (
|
|
401
|
+
not isinstance(hook["args"], list)
|
|
402
|
+
or any(not isinstance(arg, str) for arg in hook["args"])
|
|
403
|
+
):
|
|
404
|
+
msg = "Hook args must be a string list"
|
|
405
|
+
raise ValueError(msg)
|
|
406
|
+
timeout = hook.get("timeout", 10)
|
|
407
|
+
if (
|
|
408
|
+
isinstance(timeout, bool)
|
|
409
|
+
or not isinstance(timeout, (int, float))
|
|
410
|
+
or not 0 < timeout <= MAX_HOOK_TIMEOUT
|
|
411
|
+
):
|
|
412
|
+
msg = "Hook timeout must be between 0 and 60 seconds"
|
|
413
|
+
raise ValueError(msg)
|