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 @@
|
|
|
1
|
+
"""Detached-daemon support: a JSON pid registry for long-lived background processes."""
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Detached-daemon registry — a JSON file-backed pid store that survives restarts.
|
|
2
|
+
|
|
3
|
+
A *daemon* is a long-lived background process launched by the ``daemon`` tool that
|
|
4
|
+
outlives the CLI session (unlike an in-session :class:`~novacode_cli.shell.jobs.BackgroundJob`,
|
|
5
|
+
which dies with the CLI). This module tracks those daemons by name in a small JSON
|
|
6
|
+
file under ``~/.nova/daemons/registry.json`` so the agent can list, check, and stop
|
|
7
|
+
them across CLI restarts.
|
|
8
|
+
|
|
9
|
+
Because the registry outlives the process that wrote it, three things that would
|
|
10
|
+
be safe in a single session are not safe here, and each is handled explicitly:
|
|
11
|
+
|
|
12
|
+
**A pid is not an identity.** Pids are recycled, so a stale entry can name an
|
|
13
|
+
unrelated live process. Every entry records the process's real creation time and
|
|
14
|
+
:func:`is_ours` checks it before anything acts on the pid — without that,
|
|
15
|
+
stopping a long-dead daemon could force-kill a stranger's process tree.
|
|
16
|
+
|
|
17
|
+
**Another session may be writing too.** The registry is explicitly shared, so a
|
|
18
|
+
thread lock is not enough: reads and writes take a cross-process file lock, and
|
|
19
|
+
the temp file is per-process, or concurrent sessions lose each other's entries
|
|
20
|
+
(measured: 43 of 80) and can promote a half-written file over the real one.
|
|
21
|
+
|
|
22
|
+
**A name becomes a path.** Names are sanitized before they reach the filesystem;
|
|
23
|
+
``../..`` in a name would otherwise write a log outside the daemons directory.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import contextlib
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import threading
|
|
32
|
+
import time
|
|
33
|
+
from collections.abc import Iterator
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
#: Directory holding daemon logs + the registry.
|
|
39
|
+
DAEMONS_DIR = Path.home() / ".nova" / "daemons"
|
|
40
|
+
#: Registry file path.
|
|
41
|
+
REGISTRY_PATH = DAEMONS_DIR / "registry.json"
|
|
42
|
+
|
|
43
|
+
_lock = threading.RLock()
|
|
44
|
+
|
|
45
|
+
#: Maximum wait for an OS lock held by a live or stalled writer.
|
|
46
|
+
_LOCK_TIMEOUT = 5.0
|
|
47
|
+
|
|
48
|
+
#: Tolerance when matching a recorded creation time against the live process.
|
|
49
|
+
#: Windows FILETIME and the epoch conversion round; the value only needs to be
|
|
50
|
+
#: far tighter than the gap between a pid dying and being reissued.
|
|
51
|
+
_CREATE_TIME_TOLERANCE = 0.001
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _lock_path() -> Path:
|
|
55
|
+
return REGISTRY_PATH.with_name(REGISTRY_PATH.name + ".lock")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@contextlib.contextmanager
|
|
59
|
+
def _file_lock() -> Iterator[None]:
|
|
60
|
+
"""Use an OS lock that a dead process releases without unlink races."""
|
|
61
|
+
path = _lock_path()
|
|
62
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
deadline = time.monotonic() + _LOCK_TIMEOUT
|
|
64
|
+
fd = os.open(str(path), os.O_CREAT | os.O_RDWR)
|
|
65
|
+
locked = False
|
|
66
|
+
try:
|
|
67
|
+
while not locked:
|
|
68
|
+
try:
|
|
69
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
70
|
+
if os.name == "nt":
|
|
71
|
+
import msvcrt
|
|
72
|
+
|
|
73
|
+
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
|
|
74
|
+
else:
|
|
75
|
+
import fcntl
|
|
76
|
+
|
|
77
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
78
|
+
locked = True
|
|
79
|
+
except OSError as exc:
|
|
80
|
+
if time.monotonic() >= deadline:
|
|
81
|
+
raise TimeoutError("Daemon registry lock timed out") from exc
|
|
82
|
+
time.sleep(0.01)
|
|
83
|
+
yield
|
|
84
|
+
finally:
|
|
85
|
+
if locked:
|
|
86
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
87
|
+
if os.name == "nt":
|
|
88
|
+
msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
|
|
89
|
+
else:
|
|
90
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
91
|
+
os.close(fd)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def safe_name(name: str) -> str:
|
|
95
|
+
"""Reduce *name* to something that cannot escape the daemons directory.
|
|
96
|
+
|
|
97
|
+
``name`` is chosen by the agent and is interpolated into a log path, so
|
|
98
|
+
``../../x`` would otherwise write outside ``~/.nova/daemons``. Mirrors the
|
|
99
|
+
sanitizer in :mod:`novacode_cli.integrations.sandbox_registry`.
|
|
100
|
+
"""
|
|
101
|
+
cleaned = "".join(c for c in str(name) if c.isalnum() or c in "-_.")[:120]
|
|
102
|
+
# A name of dots alone still resolves to a parent directory.
|
|
103
|
+
if not cleaned or set(cleaned) <= {"."}:
|
|
104
|
+
return "unnamed"
|
|
105
|
+
return cleaned
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class DaemonInfo:
|
|
110
|
+
"""A registered daemon's metadata."""
|
|
111
|
+
|
|
112
|
+
name: str
|
|
113
|
+
pid: int
|
|
114
|
+
command: str
|
|
115
|
+
log_path: str
|
|
116
|
+
started_at: float
|
|
117
|
+
cwd: str = "."
|
|
118
|
+
#: The OS-reported process creation time, used to tell this daemon apart
|
|
119
|
+
#: from whatever later inherits its pid. 0.0 when the platform could not
|
|
120
|
+
#: report one (then only liveness is known — see :func:`is_ours`).
|
|
121
|
+
create_time: float = 0.0
|
|
122
|
+
|
|
123
|
+
def to_dict(self) -> dict[str, Any]:
|
|
124
|
+
"""Serialize to a JSON-able dict."""
|
|
125
|
+
return {
|
|
126
|
+
"name": self.name,
|
|
127
|
+
"pid": self.pid,
|
|
128
|
+
"command": self.command,
|
|
129
|
+
"log_path": self.log_path,
|
|
130
|
+
"started_at": self.started_at,
|
|
131
|
+
"cwd": self.cwd,
|
|
132
|
+
"create_time": self.create_time,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_dict(cls, data: dict[str, Any]) -> DaemonInfo:
|
|
137
|
+
"""Deserialize from a dict (as written by :meth:`to_dict`)."""
|
|
138
|
+
return cls(
|
|
139
|
+
name=str(data.get("name", "")),
|
|
140
|
+
pid=int(data.get("pid", 0)),
|
|
141
|
+
command=str(data.get("command", "")),
|
|
142
|
+
log_path=str(data.get("log_path", "")),
|
|
143
|
+
started_at=float(data.get("started_at", 0.0)),
|
|
144
|
+
cwd=str(data.get("cwd", ".")),
|
|
145
|
+
create_time=float(data.get("create_time", 0.0) or 0.0),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _read() -> dict[str, dict[str, Any]]:
|
|
150
|
+
if not REGISTRY_PATH.exists():
|
|
151
|
+
return {}
|
|
152
|
+
try:
|
|
153
|
+
data = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
|
154
|
+
if isinstance(data, dict):
|
|
155
|
+
return data
|
|
156
|
+
except (json.JSONDecodeError, OSError):
|
|
157
|
+
return {}
|
|
158
|
+
return {}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _write(data: dict[str, dict[str, Any]]) -> None:
|
|
162
|
+
DAEMONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
# Per-process temp name: a shared one lets two sessions interleave into a
|
|
164
|
+
# single half-written file and then promote it over the real registry.
|
|
165
|
+
tmp = REGISTRY_PATH.with_name(f"{REGISTRY_PATH.name}.{os.getpid()}.tmp")
|
|
166
|
+
try:
|
|
167
|
+
tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
168
|
+
tmp.replace(REGISTRY_PATH)
|
|
169
|
+
finally:
|
|
170
|
+
with contextlib.suppress(OSError):
|
|
171
|
+
if tmp.exists():
|
|
172
|
+
tmp.unlink()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def register(info: DaemonInfo) -> None:
|
|
176
|
+
"""Add or update a daemon in the registry."""
|
|
177
|
+
with _lock, _file_lock():
|
|
178
|
+
data = _read()
|
|
179
|
+
data[info.name] = info.to_dict()
|
|
180
|
+
_write(data)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def unregister(name: str) -> bool:
|
|
184
|
+
"""Remove a daemon by name. Returns True if it was present."""
|
|
185
|
+
with _lock, _file_lock():
|
|
186
|
+
data = _read()
|
|
187
|
+
if name not in data:
|
|
188
|
+
return False
|
|
189
|
+
del data[name]
|
|
190
|
+
_write(data)
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get(name: str) -> DaemonInfo | None:
|
|
195
|
+
"""Look up a daemon by name, or None."""
|
|
196
|
+
with _lock, _file_lock():
|
|
197
|
+
data = _read()
|
|
198
|
+
raw = data.get(name)
|
|
199
|
+
if raw is None:
|
|
200
|
+
return None
|
|
201
|
+
return DaemonInfo.from_dict(raw)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def list_daemons() -> list[DaemonInfo]:
|
|
205
|
+
"""Return all registered daemons, sorted by name."""
|
|
206
|
+
with _lock, _file_lock():
|
|
207
|
+
data = _read()
|
|
208
|
+
return sorted(
|
|
209
|
+
(DaemonInfo.from_dict(raw) for raw in data.values()),
|
|
210
|
+
key=lambda d: d.name,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def process_created_at(pid: int) -> float | None:
|
|
215
|
+
"""The OS-reported creation time of *pid* as a unix timestamp, or None.
|
|
216
|
+
|
|
217
|
+
None means "this platform could not tell us", not "the process is gone".
|
|
218
|
+
Windows and Linux are covered; elsewhere identity cannot be confirmed and
|
|
219
|
+
callers fall back to liveness alone.
|
|
220
|
+
"""
|
|
221
|
+
if pid <= 0:
|
|
222
|
+
return None
|
|
223
|
+
if os.name == "nt":
|
|
224
|
+
try:
|
|
225
|
+
import ctypes
|
|
226
|
+
from ctypes import wintypes
|
|
227
|
+
|
|
228
|
+
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
|
229
|
+
handle = kernel32.OpenProcess(0x1000, False, int(pid))
|
|
230
|
+
if not handle:
|
|
231
|
+
return None
|
|
232
|
+
try:
|
|
233
|
+
creation = wintypes.FILETIME()
|
|
234
|
+
exit_t, kernel_t, user_t = (
|
|
235
|
+
wintypes.FILETIME(),
|
|
236
|
+
wintypes.FILETIME(),
|
|
237
|
+
wintypes.FILETIME(),
|
|
238
|
+
)
|
|
239
|
+
ok = kernel32.GetProcessTimes(
|
|
240
|
+
handle,
|
|
241
|
+
ctypes.byref(creation),
|
|
242
|
+
ctypes.byref(exit_t),
|
|
243
|
+
ctypes.byref(kernel_t),
|
|
244
|
+
ctypes.byref(user_t),
|
|
245
|
+
)
|
|
246
|
+
if not ok:
|
|
247
|
+
return None
|
|
248
|
+
ticks = (creation.dwHighDateTime << 32) | creation.dwLowDateTime
|
|
249
|
+
# FILETIME: 100 ns units since 1601-01-01.
|
|
250
|
+
return ticks / 10_000_000.0 - 11_644_473_600.0
|
|
251
|
+
finally:
|
|
252
|
+
kernel32.CloseHandle(handle)
|
|
253
|
+
except Exception: # noqa: BLE001 — probing must never raise
|
|
254
|
+
return None
|
|
255
|
+
try:
|
|
256
|
+
# Linux: field 22 of /proc/<pid>/stat is start time in clock ticks
|
|
257
|
+
# since boot; /proc/stat's btime is boot time in epoch seconds.
|
|
258
|
+
stat = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8")
|
|
259
|
+
fields = stat[stat.rindex(")") + 2 :].split()
|
|
260
|
+
start_ticks = float(fields[19])
|
|
261
|
+
hz = os.sysconf("SC_CLK_TCK")
|
|
262
|
+
boot = 0.0
|
|
263
|
+
for line in Path("/proc/stat").read_text(encoding="utf-8").splitlines():
|
|
264
|
+
if line.startswith("btime "):
|
|
265
|
+
boot = float(line.split()[1])
|
|
266
|
+
break
|
|
267
|
+
if not boot or not hz:
|
|
268
|
+
return None
|
|
269
|
+
except (OSError, ValueError, IndexError, AttributeError):
|
|
270
|
+
return None
|
|
271
|
+
return boot + start_ticks / hz
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def is_alive(pid: int) -> bool:
|
|
275
|
+
"""True when a process with *pid* exists.
|
|
276
|
+
|
|
277
|
+
Conservative on purpose: only a *definite* "no such process" returns False.
|
|
278
|
+
An indeterminate probe (a live process we lack rights to open) must not read
|
|
279
|
+
as dead, or the caller starts a duplicate daemon and orphans the first.
|
|
280
|
+
"""
|
|
281
|
+
if pid <= 0:
|
|
282
|
+
return False
|
|
283
|
+
if os.name == "posix":
|
|
284
|
+
try:
|
|
285
|
+
os.kill(pid, 0)
|
|
286
|
+
except ProcessLookupError:
|
|
287
|
+
return False
|
|
288
|
+
except PermissionError:
|
|
289
|
+
return True # exists, owned by another user
|
|
290
|
+
except OSError:
|
|
291
|
+
return True # indeterminate — assume alive
|
|
292
|
+
# kill(pid, 0) also succeeds for Linux zombies. A shell spawned by
|
|
293
|
+
# this process can remain unreaped until another Popen cleans up, but
|
|
294
|
+
# it has exited and cannot run or respond to another stop signal.
|
|
295
|
+
try:
|
|
296
|
+
stat = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8")
|
|
297
|
+
state = stat[stat.rindex(")") + 2:].split()[0]
|
|
298
|
+
if state in {"Z", "X", "x"}:
|
|
299
|
+
return False
|
|
300
|
+
except (OSError, ValueError, IndexError):
|
|
301
|
+
pass # non-Linux POSIX or an indeterminate probe: keep liveness
|
|
302
|
+
return True
|
|
303
|
+
try:
|
|
304
|
+
import ctypes
|
|
305
|
+
|
|
306
|
+
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
|
307
|
+
handle = kernel32.OpenProcess(0x1000, False, int(pid)) # QUERY_LIMITED
|
|
308
|
+
if not handle:
|
|
309
|
+
return False
|
|
310
|
+
try:
|
|
311
|
+
code = ctypes.c_ulong()
|
|
312
|
+
if kernel32.GetExitCodeProcess(handle, ctypes.byref(code)):
|
|
313
|
+
return code.value == 259 # STILL_ACTIVE
|
|
314
|
+
return True
|
|
315
|
+
finally:
|
|
316
|
+
kernel32.CloseHandle(handle)
|
|
317
|
+
except Exception: # noqa: BLE001 — can't probe ⇒ assume alive
|
|
318
|
+
return True
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def is_ours(info: DaemonInfo) -> bool:
|
|
322
|
+
"""True when *info*'s pid is still the daemon we registered.
|
|
323
|
+
|
|
324
|
+
A live pid is not enough. Pids are reused, and this registry survives
|
|
325
|
+
reboots, so an entry can name a process that merely inherited the number —
|
|
326
|
+
acting on that would stop (or force-kill the tree of) an unrelated process.
|
|
327
|
+
"""
|
|
328
|
+
if not is_alive(info.pid):
|
|
329
|
+
return False
|
|
330
|
+
if not info.create_time:
|
|
331
|
+
# Recorded before creation times were captured, or a platform that
|
|
332
|
+
# cannot report one: liveness is all we know.
|
|
333
|
+
return True
|
|
334
|
+
actual = process_created_at(info.pid)
|
|
335
|
+
if actual is None:
|
|
336
|
+
return True
|
|
337
|
+
return abs(actual - info.create_time) <= _CREATE_TIME_TOLERANCE
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def log_path_for(name: str) -> Path:
|
|
341
|
+
"""Return the log file path for a daemon (without creating it)."""
|
|
342
|
+
return DAEMONS_DIR / f"{safe_name(name)}.log"
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Windows notifications with a click target belonging to the emitting TUI.
|
|
2
|
+
|
|
3
|
+
Uses Shell_NotifyIcon directly: no command strings, protocol registration, or
|
|
4
|
+
second Nova process is needed. The native window and icon live on one thread.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import io
|
|
10
|
+
import logging
|
|
11
|
+
import queue
|
|
12
|
+
import struct
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
ICON_PATH = Path(__file__).parent / "assets" / "Nova.png"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def icon_resource(path: Path = ICON_PATH) -> bytes:
|
|
22
|
+
"""Convert the packaged PNG to a Windows icon resource in memory."""
|
|
23
|
+
from PIL import Image
|
|
24
|
+
|
|
25
|
+
with Image.open(path) as image:
|
|
26
|
+
output = io.BytesIO()
|
|
27
|
+
image.convert("RGBA").save(output, format="ICO", sizes=[(32, 32)], bitmap_format="bmp")
|
|
28
|
+
data = output.getvalue()
|
|
29
|
+
length, offset = struct.unpack_from("<II", data, 14)
|
|
30
|
+
return data[offset : offset + length]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DesktopNotifier:
|
|
34
|
+
"""Own a bounded set of native notifications until click, timeout, or exit."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, activate, *, native=None):
|
|
37
|
+
self.activate = activate
|
|
38
|
+
self.commands = queue.Queue(maxsize=64)
|
|
39
|
+
self.closed = threading.Event()
|
|
40
|
+
self.ready = threading.Event()
|
|
41
|
+
self.native = native
|
|
42
|
+
self.thread = None
|
|
43
|
+
self.window = None
|
|
44
|
+
|
|
45
|
+
def start(self):
|
|
46
|
+
if self.thread is not None or sys.platform != "win32":
|
|
47
|
+
return
|
|
48
|
+
if self.native is None:
|
|
49
|
+
self.native = _WindowsShell()
|
|
50
|
+
self.window = self.native.capture_terminal()
|
|
51
|
+
self.thread = threading.Thread(target=self._run, name="nova-notifications", daemon=True)
|
|
52
|
+
self.thread.start()
|
|
53
|
+
|
|
54
|
+
def show(self, sid: str, notification_id: str, title: str, message: str):
|
|
55
|
+
if self.closed.is_set():
|
|
56
|
+
return
|
|
57
|
+
try:
|
|
58
|
+
self.commands.put_nowait((sid, notification_id, title, message))
|
|
59
|
+
except queue.Full:
|
|
60
|
+
logger.debug("Desktop notification queue full; retained in TUI")
|
|
61
|
+
return
|
|
62
|
+
if self.ready.is_set():
|
|
63
|
+
self.native.wake()
|
|
64
|
+
|
|
65
|
+
def close(self):
|
|
66
|
+
self.closed.set()
|
|
67
|
+
if self.ready.is_set():
|
|
68
|
+
self.native.stop()
|
|
69
|
+
if self.thread is not None and self.thread is not threading.current_thread():
|
|
70
|
+
self.thread.join(timeout=2)
|
|
71
|
+
|
|
72
|
+
def _clicked(self, sid, notification_id):
|
|
73
|
+
if self.closed.is_set():
|
|
74
|
+
return
|
|
75
|
+
self.native.focus(self.window)
|
|
76
|
+
self.activate(sid, notification_id)
|
|
77
|
+
|
|
78
|
+
def _run(self):
|
|
79
|
+
try:
|
|
80
|
+
self.native.open(icon_resource(), self._clicked)
|
|
81
|
+
self.ready.set()
|
|
82
|
+
if self.closed.is_set():
|
|
83
|
+
return
|
|
84
|
+
self.native.wake()
|
|
85
|
+
self.native.run(self.commands)
|
|
86
|
+
except Exception: # noqa: BLE001 — retain the in-terminal notification
|
|
87
|
+
logger.warning("Windows desktop notifications unavailable", exc_info=True)
|
|
88
|
+
finally:
|
|
89
|
+
self.closed.set()
|
|
90
|
+
self.native.close()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _WindowsShell:
|
|
94
|
+
"""Win32 message-loop adapter. All pointer-sized arguments are declared."""
|
|
95
|
+
|
|
96
|
+
CALLBACK = 0x8001
|
|
97
|
+
WAKE = 0x8002
|
|
98
|
+
|
|
99
|
+
def __init__(self):
|
|
100
|
+
import ctypes as c
|
|
101
|
+
from ctypes import wintypes as w
|
|
102
|
+
|
|
103
|
+
self.c, self.w = c, w
|
|
104
|
+
self.user = c.WinDLL("user32", use_last_error=True)
|
|
105
|
+
self.shell = c.WinDLL("shell32", use_last_error=True)
|
|
106
|
+
self.kernel = c.WinDLL("kernel32", use_last_error=True)
|
|
107
|
+
self.hwnd = None
|
|
108
|
+
self.icon = None
|
|
109
|
+
self.items = {}
|
|
110
|
+
self.next_id = 1
|
|
111
|
+
self.proc_type = c.WINFUNCTYPE(c.c_ssize_t, w.HWND, w.UINT, w.WPARAM, w.LPARAM)
|
|
112
|
+
|
|
113
|
+
class WindowClass(c.Structure):
|
|
114
|
+
_fields_ = [
|
|
115
|
+
("style", w.UINT),
|
|
116
|
+
("proc", self.proc_type),
|
|
117
|
+
("class_extra", c.c_int),
|
|
118
|
+
("window_extra", c.c_int),
|
|
119
|
+
("instance", w.HINSTANCE),
|
|
120
|
+
("icon", w.HICON),
|
|
121
|
+
("cursor", w.HANDLE),
|
|
122
|
+
("background", w.HBRUSH),
|
|
123
|
+
("menu", w.LPCWSTR),
|
|
124
|
+
("name", w.LPCWSTR),
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
class NotifyData(c.Structure):
|
|
128
|
+
_fields_ = [
|
|
129
|
+
("size", w.DWORD),
|
|
130
|
+
("hwnd", w.HWND),
|
|
131
|
+
("id", w.UINT),
|
|
132
|
+
("flags", w.UINT),
|
|
133
|
+
("callback", w.UINT),
|
|
134
|
+
("icon", w.HICON),
|
|
135
|
+
("tip", w.WCHAR * 128),
|
|
136
|
+
("state", w.DWORD),
|
|
137
|
+
("state_mask", w.DWORD),
|
|
138
|
+
("info", w.WCHAR * 256),
|
|
139
|
+
("version", w.UINT),
|
|
140
|
+
("title", w.WCHAR * 64),
|
|
141
|
+
("info_flags", w.DWORD),
|
|
142
|
+
("guid", c.c_byte * 16),
|
|
143
|
+
("balloon_icon", w.HICON),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
self.WindowClass, self.NotifyData = WindowClass, NotifyData
|
|
147
|
+
signatures = {
|
|
148
|
+
"GetForegroundWindow": ([], w.HWND),
|
|
149
|
+
"GetClassNameW": ([w.HWND, w.LPWSTR, c.c_int], c.c_int),
|
|
150
|
+
"GetWindowThreadProcessId": ([w.HWND, c.POINTER(w.DWORD)], w.DWORD),
|
|
151
|
+
"IsWindow": ([w.HWND], w.BOOL),
|
|
152
|
+
"ShowWindow": ([w.HWND, c.c_int], w.BOOL),
|
|
153
|
+
"SetForegroundWindow": ([w.HWND], w.BOOL),
|
|
154
|
+
"RegisterClassW": ([c.POINTER(WindowClass)], w.WORD),
|
|
155
|
+
"UnregisterClassW": ([w.LPCWSTR, w.HINSTANCE], w.BOOL),
|
|
156
|
+
"CreateWindowExW": (
|
|
157
|
+
[
|
|
158
|
+
w.DWORD,
|
|
159
|
+
w.LPCWSTR,
|
|
160
|
+
w.LPCWSTR,
|
|
161
|
+
w.DWORD,
|
|
162
|
+
c.c_int,
|
|
163
|
+
c.c_int,
|
|
164
|
+
c.c_int,
|
|
165
|
+
c.c_int,
|
|
166
|
+
w.HWND,
|
|
167
|
+
w.HMENU,
|
|
168
|
+
w.HINSTANCE,
|
|
169
|
+
w.LPVOID,
|
|
170
|
+
],
|
|
171
|
+
w.HWND,
|
|
172
|
+
),
|
|
173
|
+
"DefWindowProcW": ([w.HWND, w.UINT, w.WPARAM, w.LPARAM], c.c_ssize_t),
|
|
174
|
+
"PostMessageW": ([w.HWND, w.UINT, w.WPARAM, w.LPARAM], w.BOOL),
|
|
175
|
+
"GetMessageW": ([c.POINTER(w.MSG), w.HWND, w.UINT, w.UINT], c.c_int),
|
|
176
|
+
"TranslateMessage": ([c.POINTER(w.MSG)], w.BOOL),
|
|
177
|
+
"DispatchMessageW": ([c.POINTER(w.MSG)], c.c_ssize_t),
|
|
178
|
+
"DestroyWindow": ([w.HWND], w.BOOL),
|
|
179
|
+
"DestroyIcon": ([w.HICON], w.BOOL),
|
|
180
|
+
"CreateIconFromResourceEx": (
|
|
181
|
+
[c.POINTER(c.c_byte), w.DWORD, w.BOOL, w.DWORD, c.c_int, c.c_int, w.UINT],
|
|
182
|
+
w.HICON,
|
|
183
|
+
),
|
|
184
|
+
}
|
|
185
|
+
for name, (args, result) in signatures.items():
|
|
186
|
+
function = getattr(self.user, name)
|
|
187
|
+
function.argtypes, function.restype = args, result
|
|
188
|
+
self.kernel.GetModuleHandleW.argtypes = [w.LPCWSTR]
|
|
189
|
+
self.kernel.GetModuleHandleW.restype = w.HINSTANCE
|
|
190
|
+
self.shell.Shell_NotifyIconW.argtypes = [w.DWORD, c.POINTER(NotifyData)]
|
|
191
|
+
self.shell.Shell_NotifyIconW.restype = w.BOOL
|
|
192
|
+
|
|
193
|
+
def capture_terminal(self):
|
|
194
|
+
hwnd = self.user.GetForegroundWindow()
|
|
195
|
+
name = self.c.create_unicode_buffer(256)
|
|
196
|
+
self.user.GetClassNameW(hwnd, name, len(name))
|
|
197
|
+
if name.value not in {
|
|
198
|
+
"CASCADIA_HOSTING_WINDOW_CLASS",
|
|
199
|
+
"ConsoleWindowClass",
|
|
200
|
+
"VirtualConsoleClass",
|
|
201
|
+
}:
|
|
202
|
+
return None
|
|
203
|
+
pid = self.w.DWORD()
|
|
204
|
+
self.user.GetWindowThreadProcessId(hwnd, self.c.byref(pid))
|
|
205
|
+
return hwnd, pid.value
|
|
206
|
+
|
|
207
|
+
def focus(self, window):
|
|
208
|
+
if not window:
|
|
209
|
+
return
|
|
210
|
+
hwnd, expected_pid = window
|
|
211
|
+
pid = self.w.DWORD()
|
|
212
|
+
if self.user.IsWindow(hwnd):
|
|
213
|
+
self.user.GetWindowThreadProcessId(hwnd, self.c.byref(pid))
|
|
214
|
+
if pid.value == expected_pid:
|
|
215
|
+
self.user.ShowWindow(hwnd, 9) # SW_RESTORE
|
|
216
|
+
self.user.SetForegroundWindow(hwnd)
|
|
217
|
+
|
|
218
|
+
def open(self, resource, clicked):
|
|
219
|
+
self.clicked = clicked
|
|
220
|
+
raw = (self.c.c_byte * len(resource)).from_buffer_copy(resource)
|
|
221
|
+
self.icon = self.user.CreateIconFromResourceEx(raw, len(resource), True, 0x30000, 32, 32, 0)
|
|
222
|
+
if not self.icon:
|
|
223
|
+
raise self.c.WinError(self.c.get_last_error())
|
|
224
|
+
self.instance = self.kernel.GetModuleHandleW(None)
|
|
225
|
+
self.name = f"NovaNotifications-{id(self)}"
|
|
226
|
+
self.callback = self.proc_type(self._message)
|
|
227
|
+
wc = self.WindowClass(proc=self.callback, instance=self.instance, name=self.name)
|
|
228
|
+
if not self.user.RegisterClassW(self.c.byref(wc)):
|
|
229
|
+
raise self.c.WinError(self.c.get_last_error())
|
|
230
|
+
self.hwnd = self.user.CreateWindowExW(
|
|
231
|
+
0, self.name, "NovaCode notifications", 0, 0, 0, 0, 0, None, None, self.instance, None
|
|
232
|
+
)
|
|
233
|
+
if not self.hwnd:
|
|
234
|
+
raise self.c.WinError(self.c.get_last_error())
|
|
235
|
+
|
|
236
|
+
def _message(self, hwnd, message, uid, event):
|
|
237
|
+
if message == self.CALLBACK:
|
|
238
|
+
if event == 0x405 and uid in self.items: # NIN_BALLOONUSERCLICK
|
|
239
|
+
sid, nid = self.items[uid]
|
|
240
|
+
try:
|
|
241
|
+
self.clicked(sid, nid)
|
|
242
|
+
except Exception:
|
|
243
|
+
logger.warning("Notification click could not be routed", exc_info=True)
|
|
244
|
+
if event in (0x403, 0x404, 0x405): # hide / timeout / click
|
|
245
|
+
self._remove(uid)
|
|
246
|
+
return 0
|
|
247
|
+
return self.user.DefWindowProcW(hwnd, message, uid, event)
|
|
248
|
+
|
|
249
|
+
def _data(self, uid):
|
|
250
|
+
return self.NotifyData(size=self.c.sizeof(self.NotifyData), hwnd=self.hwnd, id=uid)
|
|
251
|
+
|
|
252
|
+
def _remove(self, uid):
|
|
253
|
+
self.shell.Shell_NotifyIconW(2, self.c.byref(self._data(uid)))
|
|
254
|
+
self.items.pop(uid, None)
|
|
255
|
+
|
|
256
|
+
def wake(self):
|
|
257
|
+
self.user.PostMessageW(self.hwnd, self.WAKE, 0, 0)
|
|
258
|
+
|
|
259
|
+
def stop(self):
|
|
260
|
+
self.user.PostMessageW(self.hwnd, 0x12, 0, 0) # WM_QUIT
|
|
261
|
+
|
|
262
|
+
def run(self, commands):
|
|
263
|
+
msg = self.w.MSG()
|
|
264
|
+
while (result := self.user.GetMessageW(self.c.byref(msg), None, 0, 0)) > 0:
|
|
265
|
+
if msg.message == self.WAKE:
|
|
266
|
+
while True:
|
|
267
|
+
try:
|
|
268
|
+
sid, nid, title, text = commands.get_nowait()
|
|
269
|
+
except queue.Empty:
|
|
270
|
+
break
|
|
271
|
+
if len(self.items) >= 32:
|
|
272
|
+
self._remove(next(iter(self.items)))
|
|
273
|
+
uid = self.next_id
|
|
274
|
+
self.next_id += 1
|
|
275
|
+
data = self._data(uid)
|
|
276
|
+
data.flags = 1 | 2 | 4 | 16 # MESSAGE | ICON | TIP | INFO
|
|
277
|
+
data.callback, data.icon, data.balloon_icon = (
|
|
278
|
+
self.CALLBACK,
|
|
279
|
+
self.icon,
|
|
280
|
+
self.icon,
|
|
281
|
+
)
|
|
282
|
+
data.tip = "NovaCode"
|
|
283
|
+
data.title, data.info = title[:63], text[:255]
|
|
284
|
+
data.info_flags = 4 | 16 | 32 | 128 # USER | NOSOUND | LARGE_ICON | QUIET_TIME
|
|
285
|
+
self.items[uid] = sid, nid
|
|
286
|
+
if not self.shell.Shell_NotifyIconW(0, self.c.byref(data)):
|
|
287
|
+
self.items.pop(uid, None)
|
|
288
|
+
else:
|
|
289
|
+
self.user.TranslateMessage(self.c.byref(msg))
|
|
290
|
+
self.user.DispatchMessageW(self.c.byref(msg))
|
|
291
|
+
if result < 0:
|
|
292
|
+
raise self.c.WinError(self.c.get_last_error())
|
|
293
|
+
|
|
294
|
+
def close(self):
|
|
295
|
+
for uid in list(self.items):
|
|
296
|
+
self._remove(uid)
|
|
297
|
+
if self.hwnd:
|
|
298
|
+
self.user.DestroyWindow(self.hwnd)
|
|
299
|
+
if self.icon:
|
|
300
|
+
self.user.DestroyIcon(self.icon)
|
|
301
|
+
if getattr(self, "name", None):
|
|
302
|
+
self.user.UnregisterClassW(self.name, self.instance)
|