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.
Files changed (691) hide show
  1. novacode_cli/__init__.py +29 -0
  2. novacode_cli/__main__.py +6 -0
  3. novacode_cli/_lazy_heavy.py +99 -0
  4. novacode_cli/_version.py +3 -0
  5. novacode_cli/agent_stream.py +178 -0
  6. novacode_cli/agents/agent_file.py +250 -0
  7. novacode_cli/agents/agent_mailbox.py +120 -0
  8. novacode_cli/agents/approval_decisions.py +146 -0
  9. novacode_cli/agents/arg_repair.py +196 -0
  10. novacode_cli/agents/async_agents/__init__.py +6 -0
  11. novacode_cli/agents/async_agents/_config.py +213 -0
  12. novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
  13. novacode_cli/agents/async_agents/_specialist.py +84 -0
  14. novacode_cli/agents/async_agents/_test_commands.py +89 -0
  15. novacode_cli/agents/async_agents/_tools.py +432 -0
  16. novacode_cli/agents/async_agents/general_purpose.py +42 -0
  17. novacode_cli/agents/async_agents/langgraph.json +9 -0
  18. novacode_cli/agents/async_agents/research.py +22 -0
  19. novacode_cli/agents/async_context.py +303 -0
  20. novacode_cli/agents/async_on_demand.py +52 -0
  21. novacode_cli/agents/async_task_errors.py +75 -0
  22. novacode_cli/agents/async_workspace.py +27 -0
  23. novacode_cli/agents/attribution.py +165 -0
  24. novacode_cli/agents/browser_mode.py +119 -0
  25. novacode_cli/agents/btw_agent.py +71 -0
  26. novacode_cli/agents/caveman.py +352 -0
  27. novacode_cli/agents/caveman_support.py +370 -0
  28. novacode_cli/agents/code_mode.py +774 -0
  29. novacode_cli/agents/code_runtime.py +160 -0
  30. novacode_cli/agents/context_compaction.py +1339 -0
  31. novacode_cli/agents/core_agent.py +2710 -0
  32. novacode_cli/agents/default_subagents/__init__.py +24 -0
  33. novacode_cli/agents/default_subagents/async_subagents.py +296 -0
  34. novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
  35. novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
  36. novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
  37. novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
  38. novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
  39. novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
  40. novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
  41. novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
  42. novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
  43. novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
  44. novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
  45. novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
  46. novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
  47. novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
  48. novacode_cli/agents/default_subagents/prompt.py +88 -0
  49. novacode_cli/agents/default_subagents/subagents.py +138 -0
  50. novacode_cli/agents/dream_agent.py +93 -0
  51. novacode_cli/agents/edit_diagnostics.py +362 -0
  52. novacode_cli/agents/edit_fallback.py +62 -0
  53. novacode_cli/agents/handoff.py +166 -0
  54. novacode_cli/agents/invocation.py +152 -0
  55. novacode_cli/agents/model_budget.py +42 -0
  56. novacode_cli/agents/model_retry.py +113 -0
  57. novacode_cli/agents/model_router.py +420 -0
  58. novacode_cli/agents/openai_decisions.py +186 -0
  59. novacode_cli/agents/plan_agent/__init__.py +12 -0
  60. novacode_cli/agents/plan_agent/plan_agent.py +357 -0
  61. novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
  62. novacode_cli/agents/plan_execution.py +237 -0
  63. novacode_cli/agents/ponytail.py +66 -0
  64. novacode_cli/agents/prime.py +274 -0
  65. novacode_cli/agents/provider_caching.py +99 -0
  66. novacode_cli/agents/server_launcher.py +904 -0
  67. novacode_cli/agents/strict_tools.py +384 -0
  68. novacode_cli/agents/task_discipline.py +294 -0
  69. novacode_cli/agents/tool_call_repair.py +290 -0
  70. novacode_cli/agents/tool_offload.py +291 -0
  71. novacode_cli/agents/tool_search.py +806 -0
  72. novacode_cli/agents/tool_search_variants.py +106 -0
  73. novacode_cli/agents/tool_verdicts.py +1194 -0
  74. novacode_cli/agents/user_async_agents.py +312 -0
  75. novacode_cli/artifacts/__init__.py +10 -0
  76. novacode_cli/artifacts/registry.py +278 -0
  77. novacode_cli/artifacts/server.py +426 -0
  78. novacode_cli/assets/Nova.png +0 -0
  79. novacode_cli/assets/nova-avatar.png +0 -0
  80. novacode_cli/audio/__init__.py +301 -0
  81. novacode_cli/audio/capture.py +137 -0
  82. novacode_cli/audio/downloads.py +273 -0
  83. novacode_cli/audio/pipeline.py +391 -0
  84. novacode_cli/audio/playback.py +87 -0
  85. novacode_cli/audio/providers.py +208 -0
  86. novacode_cli/audio/speakable.py +78 -0
  87. novacode_cli/audio/stt.py +132 -0
  88. novacode_cli/audio/stt_deepgram.py +69 -0
  89. novacode_cli/audio/stt_elevenlabs.py +89 -0
  90. novacode_cli/audio/stt_parakeet.py +94 -0
  91. novacode_cli/audio/stt_whistle.py +61 -0
  92. novacode_cli/audio/summarize.py +92 -0
  93. novacode_cli/audio/tts.py +85 -0
  94. novacode_cli/audio/tts_kokoro.py +73 -0
  95. novacode_cli/audio/tts_orpheus.py +180 -0
  96. novacode_cli/audio/tts_pocket.py +131 -0
  97. novacode_cli/audio/tts_sano.py +69 -0
  98. novacode_cli/audio/vad.py +145 -0
  99. novacode_cli/backends/__init__.py +17 -0
  100. novacode_cli/backends/filesystem.py +894 -0
  101. novacode_cli/backends/native_search.py +92 -0
  102. novacode_cli/bootstrap/__init__.py +9 -0
  103. novacode_cli/bootstrap/env_snapshot.py +240 -0
  104. novacode_cli/bootstrap/steering.py +464 -0
  105. novacode_cli/bootstrap/vision_router.py +604 -0
  106. novacode_cli/brand.py +362 -0
  107. novacode_cli/cli_args.py +718 -0
  108. novacode_cli/commands/__init__.py +237 -0
  109. novacode_cli/commands/agents_commands.py +231 -0
  110. novacode_cli/commands/auth_handler.py +40 -0
  111. novacode_cli/commands/browser_use_handler.py +75 -0
  112. novacode_cli/commands/cache_handler.py +32 -0
  113. novacode_cli/commands/chat_handler.py +1357 -0
  114. novacode_cli/commands/ci_fix.py +171 -0
  115. novacode_cli/commands/claude_plan.py +331 -0
  116. novacode_cli/commands/codex_plan.py +195 -0
  117. novacode_cli/commands/commands.py +1113 -0
  118. novacode_cli/commands/create_handler.py +124 -0
  119. novacode_cli/commands/create_server.py +1887 -0
  120. novacode_cli/commands/cron_handler.py +119 -0
  121. novacode_cli/commands/dream_handler.py +203 -0
  122. novacode_cli/commands/effort_handler.py +65 -0
  123. novacode_cli/commands/evolution_handler.py +214 -0
  124. novacode_cli/commands/file_commands.py +381 -0
  125. novacode_cli/commands/handoff_handler.py +68 -0
  126. novacode_cli/commands/hooks_handler.py +269 -0
  127. novacode_cli/commands/init_handler.py +146 -0
  128. novacode_cli/commands/learning_handler.py +160 -0
  129. novacode_cli/commands/lite_mode_handler.py +76 -0
  130. novacode_cli/commands/log_commands.py +363 -0
  131. novacode_cli/commands/mcp_handler.py +29 -0
  132. novacode_cli/commands/model_handler.py +29 -0
  133. novacode_cli/commands/notifications_handler.py +158 -0
  134. novacode_cli/commands/onboard_handler.py +121 -0
  135. novacode_cli/commands/plan_handler.py +440 -0
  136. novacode_cli/commands/planner_usage.py +250 -0
  137. novacode_cli/commands/plugin_install_handler.py +38 -0
  138. novacode_cli/commands/plugins_handler.py +35 -0
  139. novacode_cli/commands/prime_handler.py +455 -0
  140. novacode_cli/commands/prompt_handler.py +149 -0
  141. novacode_cli/commands/provider_usage.py +160 -0
  142. novacode_cli/commands/ralph_events.py +106 -0
  143. novacode_cli/commands/ralph_handler.py +1293 -0
  144. novacode_cli/commands/refine_handler.py +213 -0
  145. novacode_cli/commands/research_handler.py +202 -0
  146. novacode_cli/commands/rootapp_remote.py +83 -0
  147. novacode_cli/commands/router_handler.py +36 -0
  148. novacode_cli/commands/server_commands.py +249 -0
  149. novacode_cli/commands/session_commands.py +203 -0
  150. novacode_cli/commands/share_handler.py +450 -0
  151. novacode_cli/commands/side_commands.py +366 -0
  152. novacode_cli/commands/skill_invoke.py +420 -0
  153. novacode_cli/commands/skills_commands.py +48 -0
  154. novacode_cli/commands/trace_handler.py +322 -0
  155. novacode_cli/commands/trello_handler.py +226 -0
  156. novacode_cli/commands/trello_remote.py +177 -0
  157. novacode_cli/commands/trello_server.py +1214 -0
  158. novacode_cli/commands/vision_handler.py +127 -0
  159. novacode_cli/commands/voice_handler.py +447 -0
  160. novacode_cli/commands/webhook_handler.py +134 -0
  161. novacode_cli/commands/wiki_commands.py +227 -0
  162. novacode_cli/compaction.py +753 -0
  163. novacode_cli/computation_cache.py +159 -0
  164. novacode_cli/computer/__init__.py +1 -0
  165. novacode_cli/computer/persistent.py +234 -0
  166. novacode_cli/computer/preview.py +253 -0
  167. novacode_cli/computer/runtime.py +474 -0
  168. novacode_cli/computer/windows_console.py +152 -0
  169. novacode_cli/computer/windows_qemu.py +211 -0
  170. novacode_cli/config/browser_mode.py +159 -0
  171. novacode_cli/config/catalog_models.py +95 -0
  172. novacode_cli/config/config.py +1388 -0
  173. novacode_cli/config/credentials.py +540 -0
  174. novacode_cli/config/google_oauth_auth.py +281 -0
  175. novacode_cli/config/model_capabilities.py +343 -0
  176. novacode_cli/config/model_catalog.py +275 -0
  177. novacode_cli/config/model_create.py +1325 -0
  178. novacode_cli/config/model_manager.py +695 -0
  179. novacode_cli/config/nova_config.py +1283 -0
  180. novacode_cli/config/openai_chatgpt_auth.py +384 -0
  181. novacode_cli/config/opencode_gateway.py +25 -0
  182. novacode_cli/config/plan_mode.py +188 -0
  183. novacode_cli/config/provider_auth.py +438 -0
  184. novacode_cli/config/provider_catalog.json +5127 -0
  185. novacode_cli/config/provider_registry.py +38 -0
  186. novacode_cli/config/provider_transports.py +258 -0
  187. novacode_cli/config/role_models.py +175 -0
  188. novacode_cli/context/__init__.py +50 -0
  189. novacode_cli/context/_analysis.py +799 -0
  190. novacode_cli/context/_budget.py +116 -0
  191. novacode_cli/context/_conversation.py +73 -0
  192. novacode_cli/context/_dynamic.py +264 -0
  193. novacode_cli/context/_model_config.py +380 -0
  194. novacode_cli/context/_models_dev.py +99 -0
  195. novacode_cli/context/archives.py +248 -0
  196. novacode_cli/context/estimates.py +65 -0
  197. novacode_cli/context/history.py +17 -0
  198. novacode_cli/context/knowledge_cache.py +292 -0
  199. novacode_cli/context/manager.py +101 -0
  200. novacode_cli/context/observations.py +65 -0
  201. novacode_cli/context/pressure.py +168 -0
  202. novacode_cli/context/provider_accounting.py +109 -0
  203. novacode_cli/context/snapshot.py +493 -0
  204. novacode_cli/context/strategies.py +279 -0
  205. novacode_cli/context/task_progress.py +122 -0
  206. novacode_cli/core/__init__.py +0 -0
  207. novacode_cli/core/agent_loop.py +1821 -0
  208. novacode_cli/core/autonomous_loop.py +277 -0
  209. novacode_cli/core/code_approvals.py +57 -0
  210. novacode_cli/core/input_preparation.py +276 -0
  211. novacode_cli/core/reasoning.py +67 -0
  212. novacode_cli/core/streaming.py +326 -0
  213. novacode_cli/core/subagent_tasks.py +696 -0
  214. novacode_cli/core/subagent_tracking.py +235 -0
  215. novacode_cli/core/tool_results.py +116 -0
  216. novacode_cli/core/verification_loop.py +272 -0
  217. novacode_cli/council.py +595 -0
  218. novacode_cli/council_consensus.py +115 -0
  219. novacode_cli/council_decision.py +157 -0
  220. novacode_cli/council_planner.py +1421 -0
  221. novacode_cli/council_settings.py +208 -0
  222. novacode_cli/council_web_settings.py +41 -0
  223. novacode_cli/council_workspace.py +56 -0
  224. novacode_cli/cowork/__init__.py +10 -0
  225. novacode_cli/cowork/broker_middleware.py +162 -0
  226. novacode_cli/cowork/launcher.py +102 -0
  227. novacode_cli/cowork/policy.py +284 -0
  228. novacode_cli/cowork/ui.py +826 -0
  229. novacode_cli/daemons/__init__.py +1 -0
  230. novacode_cli/daemons/registry.py +342 -0
  231. novacode_cli/desktop_notifications.py +302 -0
  232. novacode_cli/doctor.py +251 -0
  233. novacode_cli/embeddings.py +109 -0
  234. novacode_cli/entrypoint.py +56 -0
  235. novacode_cli/errors/__init__.py +19 -0
  236. novacode_cli/errors/handlers.py +648 -0
  237. novacode_cli/errors/provider_errors.py +249 -0
  238. novacode_cli/errors/taxonomy.py +80 -0
  239. novacode_cli/events.py +179 -0
  240. novacode_cli/file_ops.py +714 -0
  241. novacode_cli/git_safety.py +281 -0
  242. novacode_cli/handoff.py +383 -0
  243. novacode_cli/headless/__init__.py +18 -0
  244. novacode_cli/headless/input.py +58 -0
  245. novacode_cli/headless/output.py +224 -0
  246. novacode_cli/headless/pipe.py +532 -0
  247. novacode_cli/headless/runner.py +461 -0
  248. novacode_cli/hermes/__init__.py +39 -0
  249. novacode_cli/hermes/activation.py +297 -0
  250. novacode_cli/hermes/config.py +129 -0
  251. novacode_cli/hermes/curator.py +199 -0
  252. novacode_cli/hermes/evolution.py +374 -0
  253. novacode_cli/hermes/ledger.py +364 -0
  254. novacode_cli/hermes/memory_tiers.py +969 -0
  255. novacode_cli/hermes/middleware.py +678 -0
  256. novacode_cli/hermes/overview.py +174 -0
  257. novacode_cli/hermes/prompt_evolution.py +503 -0
  258. novacode_cli/hermes/refine_loop.py +643 -0
  259. novacode_cli/hermes/refinement_log.py +190 -0
  260. novacode_cli/hermes/review.py +674 -0
  261. novacode_cli/hermes/skill_debate.py +135 -0
  262. novacode_cli/hermes/skill_discovery.py +450 -0
  263. novacode_cli/hermes/skill_manager.py +335 -0
  264. novacode_cli/hermes/skill_proposals.py +43 -0
  265. novacode_cli/hermes/test_gaming.py +119 -0
  266. novacode_cli/hermes/tracker.py +435 -0
  267. novacode_cli/hermes/tuner.py +206 -0
  268. novacode_cli/hermes/verification_memory.py +134 -0
  269. novacode_cli/hermes/verifier.py +277 -0
  270. novacode_cli/hitl/__init__.py +22 -0
  271. novacode_cli/hitl/interrupts.py +493 -0
  272. novacode_cli/hooks.py +359 -0
  273. novacode_cli/image_utils.py +517 -0
  274. novacode_cli/init/__init__.py +7 -0
  275. novacode_cli/init/events.py +43 -0
  276. novacode_cli/input_utils.py +526 -0
  277. novacode_cli/install_commands.py +87 -0
  278. novacode_cli/integrations/__init__.py +82 -0
  279. novacode_cli/integrations/daytona.py +122 -0
  280. novacode_cli/integrations/docker.py +309 -0
  281. novacode_cli/integrations/host_path.py +111 -0
  282. novacode_cli/integrations/langsmith.py +151 -0
  283. novacode_cli/integrations/modal.py +134 -0
  284. novacode_cli/integrations/runloop.py +130 -0
  285. novacode_cli/integrations/sandbox_factory.py +1388 -0
  286. novacode_cli/integrations/sandbox_registry.py +312 -0
  287. novacode_cli/integrations/workdir_backend.py +589 -0
  288. novacode_cli/main.py +2078 -0
  289. novacode_cli/mcp/__init__.py +73 -0
  290. novacode_cli/mcp/auth.py +189 -0
  291. novacode_cli/mcp/client.py +571 -0
  292. novacode_cli/mcp/code_mode.py +5 -0
  293. novacode_cli/mcp/code_runtime.py +5 -0
  294. novacode_cli/mcp/commands.py +902 -0
  295. novacode_cli/mcp/config.py +406 -0
  296. novacode_cli/mcp/generic.py +128 -0
  297. novacode_cli/mcp/middleware.py +993 -0
  298. novacode_cli/mcp/presets.py +367 -0
  299. novacode_cli/memory/__init__.py +1 -0
  300. novacode_cli/memory/agent_memory.py +1312 -0
  301. novacode_cli/memory/builtin_lessons.py +60 -0
  302. novacode_cli/memory/dream_gates.py +192 -0
  303. novacode_cli/memory/limits.py +59 -0
  304. novacode_cli/memory/parsed_cache.py +106 -0
  305. novacode_cli/memory/recall_stats.py +97 -0
  306. novacode_cli/memory/semantic.py +116 -0
  307. novacode_cli/memory/store.py +335 -0
  308. novacode_cli/memory/wiki.py +274 -0
  309. novacode_cli/migrate.py +185 -0
  310. novacode_cli/onboarding.py +649 -0
  311. novacode_cli/onboarding_check.py +119 -0
  312. novacode_cli/path_approval.py +297 -0
  313. novacode_cli/plan_archive.py +148 -0
  314. novacode_cli/plugins/__init__.py +121 -0
  315. novacode_cli/plugins/claude_plugins.py +538 -0
  316. novacode_cli/plugins/cli.py +168 -0
  317. novacode_cli/plugins/components.py +413 -0
  318. novacode_cli/plugins/hooks.py +356 -0
  319. novacode_cli/plugins/loader.py +317 -0
  320. novacode_cli/plugins/marketplaces.py +199 -0
  321. novacode_cli/plugins/storage.py +70 -0
  322. novacode_cli/prime.py +759 -0
  323. novacode_cli/prime_factory/__init__.py +15 -0
  324. novacode_cli/prime_factory/container.py +198 -0
  325. novacode_cli/prime_factory/controller.py +565 -0
  326. novacode_cli/prime_factory/knowledge.py +261 -0
  327. novacode_cli/prime_factory/publication.py +218 -0
  328. novacode_cli/prime_factory/recovery.py +50 -0
  329. novacode_cli/prime_factory/research.py +370 -0
  330. novacode_cli/prime_factory/runner.py +608 -0
  331. novacode_cli/prime_factory/runtime.py +261 -0
  332. novacode_cli/prime_factory/state.py +201 -0
  333. novacode_cli/prime_factory/store.py +128 -0
  334. novacode_cli/prime_factory/worker.py +129 -0
  335. novacode_cli/prime_factory/workflow.py +899 -0
  336. novacode_cli/prime_factory/workspace.py +317 -0
  337. novacode_cli/prime_memory.py +290 -0
  338. novacode_cli/process_manager.py +585 -0
  339. novacode_cli/project_onboard.py +537 -0
  340. novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
  341. novacode_cli/prompts/__init__.py +168 -0
  342. novacode_cli/prompts/agent_definitions.jinja +11 -0
  343. novacode_cli/prompts/agent_generation.jinja +125 -0
  344. novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
  345. novacode_cli/prompts/claude_plan_mode.jinja +12 -0
  346. novacode_cli/prompts/coding_workflow.jinja +18 -0
  347. novacode_cli/prompts/computer_use.jinja +17 -0
  348. novacode_cli/prompts/core_agent_system.jinja +81 -0
  349. novacode_cli/prompts/delegation.jinja +15 -0
  350. novacode_cli/prompts/desktop_power.jinja +4 -0
  351. novacode_cli/prompts/file_paths.jinja +15 -0
  352. novacode_cli/prompts/file_tracker.jinja +23 -0
  353. novacode_cli/prompts/init_agents_md.jinja +56 -0
  354. novacode_cli/prompts/init_exploration.jinja +174 -0
  355. novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
  356. novacode_cli/prompts/instruction_scope.jinja +5 -0
  357. novacode_cli/prompts/learnings_extraction.jinja +45 -0
  358. novacode_cli/prompts/longterm_memory.jinja +54 -0
  359. novacode_cli/prompts/mcp.jinja +84 -0
  360. novacode_cli/prompts/memory_consolidation.jinja +40 -0
  361. novacode_cli/prompts/nova_evolution.jinja +73 -0
  362. novacode_cli/prompts/nova_review.jinja +158 -0
  363. novacode_cli/prompts/nova_skill_debate.jinja +36 -0
  364. novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
  365. novacode_cli/prompts/nova_verify.jinja +100 -0
  366. novacode_cli/prompts/onboard_dashboard.jinja +378 -0
  367. novacode_cli/prompts/plan_agent.jinja +87 -0
  368. novacode_cli/prompts/plan_scouts.jinja +5 -0
  369. novacode_cli/prompts/plan_storage.jinja +8 -0
  370. novacode_cli/prompts/planning.jinja +82 -0
  371. novacode_cli/prompts/ponytail/LICENSE +21 -0
  372. novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
  373. novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
  374. novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
  375. novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
  376. novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
  377. novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
  378. novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
  379. novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
  380. novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
  381. novacode_cli/prompts/prompt_evolution.jinja +27 -0
  382. novacode_cli/prompts/ralph_iteration.jinja +75 -0
  383. novacode_cli/prompts/refine_gate.jinja +31 -0
  384. novacode_cli/prompts/refine_plan.jinja +64 -0
  385. novacode_cli/prompts/refine_review.jinja +34 -0
  386. novacode_cli/prompts/research_delivery.jinja +9 -0
  387. novacode_cli/prompts/research_swarm.jinja +130 -0
  388. novacode_cli/prompts/response_style.jinja +5 -0
  389. novacode_cli/prompts/session_summarization.jinja +86 -0
  390. novacode_cli/prompts/skill_creation.jinja +85 -0
  391. novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
  392. novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
  393. novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
  394. novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
  395. novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
  396. novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
  397. novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
  398. novacode_cli/prompts/summarization.jinja +107 -0
  399. novacode_cli/prompts/summarization_answers.jinja +23 -0
  400. novacode_cli/prompts/summarization_questions.jinja +32 -0
  401. novacode_cli/prompts/vision_caption.jinja +21 -0
  402. novacode_cli/prompts/voice_summary.jinja +65 -0
  403. novacode_cli/prompts/wiki_file.jinja +57 -0
  404. novacode_cli/prompts/wiki_ingest.jinja +87 -0
  405. novacode_cli/recovery.py +383 -0
  406. novacode_cli/remote/__init__.py +45 -0
  407. novacode_cli/remote/approvals.py +282 -0
  408. novacode_cli/remote/async_task_clients.py +97 -0
  409. novacode_cli/remote/async_task_watcher.py +240 -0
  410. novacode_cli/remote/bridge.py +905 -0
  411. novacode_cli/remote/command_menu.py +26 -0
  412. novacode_cli/remote/command_ui.py +243 -0
  413. novacode_cli/remote/config.py +199 -0
  414. novacode_cli/remote/discord_bridge.py +530 -0
  415. novacode_cli/remote/discord_format.py +31 -0
  416. novacode_cli/remote/discord_threads.py +225 -0
  417. novacode_cli/remote/images.py +97 -0
  418. novacode_cli/remote/processor.py +601 -0
  419. novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
  420. novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
  421. novacode_cli/remote/rootapp_bot/package.json +18 -0
  422. novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
  423. novacode_cli/remote/rootapp_bridge.py +370 -0
  424. novacode_cli/remote/rootapp_setup.py +89 -0
  425. novacode_cli/remote/routing.py +103 -0
  426. novacode_cli/remote/scheduler.py +302 -0
  427. novacode_cli/remote/session.py +164 -0
  428. novacode_cli/remote/status.py +411 -0
  429. novacode_cli/remote/streaming.py +188 -0
  430. novacode_cli/remote/telegram_bridge.py +1118 -0
  431. novacode_cli/remote/telegram_format.py +215 -0
  432. novacode_cli/remote/telegram_hub.py +256 -0
  433. novacode_cli/remote/voice_notes.py +120 -0
  434. novacode_cli/remote/webhook_adapters.py +167 -0
  435. novacode_cli/remote/webhook_server.py +154 -0
  436. novacode_cli/rewind.py +150 -0
  437. novacode_cli/rooms/__init__.py +1 -0
  438. novacode_cli/rooms/bridge.py +86 -0
  439. novacode_cli/rooms/cli.py +231 -0
  440. novacode_cli/rooms/nova.py +227 -0
  441. novacode_cli/rooms/runtime.py +299 -0
  442. novacode_cli/rooms/store.py +538 -0
  443. novacode_cli/rooms/workspace.py +94 -0
  444. novacode_cli/security/__init__.py +29 -0
  445. novacode_cli/security/approval_context.py +45 -0
  446. novacode_cli/security/delegated_approval.py +68 -0
  447. novacode_cli/security/local_http.py +85 -0
  448. novacode_cli/security/middleware.py +87 -0
  449. novacode_cli/security/paths.py +35 -0
  450. novacode_cli/security/policy.py +535 -0
  451. novacode_cli/security/policy_writer.py +90 -0
  452. novacode_cli/security/prompt_injection.py +203 -0
  453. novacode_cli/security/remember.py +81 -0
  454. novacode_cli/security/rule_synthesis.py +116 -0
  455. novacode_cli/security/secret_files.py +61 -0
  456. novacode_cli/security/session_allow.py +114 -0
  457. novacode_cli/security/unicode_security.py +516 -0
  458. novacode_cli/security/validator.py +173 -0
  459. novacode_cli/server/__init__.py +9 -0
  460. novacode_cli/server/app.py +632 -0
  461. novacode_cli/server/event_adapter.py +189 -0
  462. novacode_cli/server/session_manager.py +90 -0
  463. novacode_cli/server_main.py +64 -0
  464. novacode_cli/server_runner/dev_server.py +530 -0
  465. novacode_cli/server_runner/test_runner.py +470 -0
  466. novacode_cli/session/__init__.py +1 -0
  467. novacode_cli/session/adapters/__init__.py +240 -0
  468. novacode_cli/session/adapters/claude_code.py +102 -0
  469. novacode_cli/session/adapters/codex.py +77 -0
  470. novacode_cli/session/adapters/harness_native.py +75 -0
  471. novacode_cli/session/checkpoint_coordinator.py +448 -0
  472. novacode_cli/session/context_restore.py +25 -0
  473. novacode_cli/session/history_export.py +228 -0
  474. novacode_cli/session/history_tree.py +174 -0
  475. novacode_cli/session/imported_context.py +267 -0
  476. novacode_cli/session/runtime_identity.py +32 -0
  477. novacode_cli/session/session_persistence.py +1168 -0
  478. novacode_cli/session/session_prompt_builder.py +279 -0
  479. novacode_cli/session/session_restore.py +351 -0
  480. novacode_cli/session/session_summarization.py +20 -0
  481. novacode_cli/session/transcript_journal.py +153 -0
  482. novacode_cli/sessions/__init__.py +11 -0
  483. novacode_cli/sessions/gateway.py +84 -0
  484. novacode_cli/sessions/launch.py +51 -0
  485. novacode_cli/sessions/lease.py +213 -0
  486. novacode_cli/sessions/messaging.py +255 -0
  487. novacode_cli/sessions/protocol.py +216 -0
  488. novacode_cli/sessions/resume.py +31 -0
  489. novacode_cli/sessions/supervisor.py +498 -0
  490. novacode_cli/sessions/tasks.py +73 -0
  491. novacode_cli/sessions/tui_messaging.py +413 -0
  492. novacode_cli/sessions/worker.py +652 -0
  493. novacode_cli/sessions/worktree.py +262 -0
  494. novacode_cli/shell/__init__.py +27 -0
  495. novacode_cli/shell/jobs.py +475 -0
  496. novacode_cli/shell/middleware.py +1568 -0
  497. novacode_cli/shell/os_sandbox.py +205 -0
  498. novacode_cli/shell/output.py +181 -0
  499. novacode_cli/shell/patterns.py +275 -0
  500. novacode_cli/shell/processes.py +87 -0
  501. novacode_cli/shell/utils.py +236 -0
  502. novacode_cli/skills/__init__.py +26 -0
  503. novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
  504. novacode_cli/skills/curation_middleware.py +69 -0
  505. novacode_cli/skills/executable.py +161 -0
  506. novacode_cli/skills/libraries.py +102 -0
  507. novacode_cli/skills/load.py +248 -0
  508. novacode_cli/skills/prune.py +215 -0
  509. novacode_cli/skills/refreshing_middleware.py +896 -0
  510. novacode_cli/skills/retrieval.py +248 -0
  511. novacode_cli/skills/runtime.py +118 -0
  512. novacode_cli/skills/schema.py +254 -0
  513. novacode_cli/skills/skill_creation.py +1549 -0
  514. novacode_cli/skills/skill_lock.py +75 -0
  515. novacode_cli/skills/skill_system_prompt.py +16 -0
  516. novacode_cli/skills/skills_prefs.py +126 -0
  517. novacode_cli/skills/tool_resolver.py +31 -0
  518. novacode_cli/skills/upstream.py +50 -0
  519. novacode_cli/skills/versioning.py +291 -0
  520. novacode_cli/states/Session.py +847 -0
  521. novacode_cli/states/__init__.py +31 -0
  522. novacode_cli/states/slices/__init__.py +17 -0
  523. novacode_cli/states/slices/agent_runtime.py +249 -0
  524. novacode_cli/states/slices/background_tasks.py +21 -0
  525. novacode_cli/states/slices/notifications.py +74 -0
  526. novacode_cli/states/slices/remote_bridge.py +56 -0
  527. novacode_cli/states/slices/ui_settings.py +49 -0
  528. novacode_cli/states/slices/wiki.py +25 -0
  529. novacode_cli/teammates/__init__.py +1 -0
  530. novacode_cli/teammates/controller.py +557 -0
  531. novacode_cli/teammates/graph.py +104 -0
  532. novacode_cli/teammates/registry.py +175 -0
  533. novacode_cli/teammates/service.py +254 -0
  534. novacode_cli/teammates/worker.py +19 -0
  535. novacode_cli/teams/__init__.py +1 -0
  536. novacode_cli/teams/adapters/__init__.py +1 -0
  537. novacode_cli/teams/adapters/container.py +316 -0
  538. novacode_cli/teams/adapters/inprocess.py +206 -0
  539. novacode_cli/teams/adapters/process.py +364 -0
  540. novacode_cli/teams/bootstrap.py +246 -0
  541. novacode_cli/teams/container_git.py +67 -0
  542. novacode_cli/teams/context.py +34 -0
  543. novacode_cli/teams/coordinator.py +815 -0
  544. novacode_cli/teams/gateway.py +200 -0
  545. novacode_cli/teams/host.py +64 -0
  546. novacode_cli/teams/legacy.py +52 -0
  547. novacode_cli/teams/middleware.py +176 -0
  548. novacode_cli/teams/models.py +104 -0
  549. novacode_cli/teams/output.py +47 -0
  550. novacode_cli/teams/protocol.py +50 -0
  551. novacode_cli/teams/recovery.py +80 -0
  552. novacode_cli/teams/scheduler.py +44 -0
  553. novacode_cli/teams/store.py +631 -0
  554. novacode_cli/teams/worker.py +251 -0
  555. novacode_cli/teams/workspace.py +100 -0
  556. novacode_cli/token_utils.py +256 -0
  557. novacode_cli/tools/__init__.py +104 -0
  558. novacode_cli/tools/_shared.py +97 -0
  559. novacode_cli/tools/_types.py +24 -0
  560. novacode_cli/tools/artifact_tools.py +137 -0
  561. novacode_cli/tools/code_search_tools.py +330 -0
  562. novacode_cli/tools/computer_tools.py +77 -0
  563. novacode_cli/tools/cron_tools.py +65 -0
  564. novacode_cli/tools/daemon_tool.py +290 -0
  565. novacode_cli/tools/desktop_tools.py +161 -0
  566. novacode_cli/tools/fetch_tools.py +530 -0
  567. novacode_cli/tools/http_tools.py +279 -0
  568. novacode_cli/tools/inspection_tools.py +107 -0
  569. novacode_cli/tools/job_tools.py +143 -0
  570. novacode_cli/tools/knowledge_tools.py +427 -0
  571. novacode_cli/tools/memory_tools.py +461 -0
  572. novacode_cli/tools/memory_wiki_tools.py +40 -0
  573. novacode_cli/tools/notebook_tools.py +238 -0
  574. novacode_cli/tools/oracle_tool.py +83 -0
  575. novacode_cli/tools/plan_archive_tools.py +80 -0
  576. novacode_cli/tools/plan_mode_tools.py +315 -0
  577. novacode_cli/tools/python_kernel_loop.py +165 -0
  578. novacode_cli/tools/python_kernel_tool.py +222 -0
  579. novacode_cli/tools/reflection_tools.py +34 -0
  580. novacode_cli/tools/scraper_tools.py +377 -0
  581. novacode_cli/tools/skill_tools.py +275 -0
  582. novacode_cli/tools/speak_tool.py +51 -0
  583. novacode_cli/tools/store_memory_tools.py +227 -0
  584. novacode_cli/tools/ui_tools.py +62 -0
  585. novacode_cli/tools/waiter_tools.py +408 -0
  586. novacode_cli/tools/web_tools.py +349 -0
  587. novacode_cli/tools/wiki_tools.py +152 -0
  588. novacode_cli/tools/worktree_tools.py +219 -0
  589. novacode_cli/tracking/__init__.py +10 -0
  590. novacode_cli/tracking/file_tracker.py +826 -0
  591. novacode_cli/tracking/local_metrics.py +170 -0
  592. novacode_cli/tracking/loop_guard.py +507 -0
  593. novacode_cli/tracking/request_metrics.py +60 -0
  594. novacode_cli/tracking/tracing.py +692 -0
  595. novacode_cli/tracking/usage_tree.py +677 -0
  596. novacode_cli/tracking/workspace_anchoring.py +231 -0
  597. novacode_cli/tui/__init__.py +11 -0
  598. novacode_cli/tui/animation_rate.py +9 -0
  599. novacode_cli/tui/animations.py +332 -0
  600. novacode_cli/tui/app.py +19315 -0
  601. novacode_cli/tui/auth_display.py +87 -0
  602. novacode_cli/tui/auth_screens.py +826 -0
  603. novacode_cli/tui/avatar.py +294 -0
  604. novacode_cli/tui/avatar_frames.py +21 -0
  605. novacode_cli/tui/background_tasks.py +35 -0
  606. novacode_cli/tui/browser_settings_screen.py +70 -0
  607. novacode_cli/tui/caveman_screen.py +185 -0
  608. novacode_cli/tui/council_settings_screen.py +182 -0
  609. novacode_cli/tui/council_view.py +535 -0
  610. novacode_cli/tui/decision_settings.py +268 -0
  611. novacode_cli/tui/diff_preview.py +199 -0
  612. novacode_cli/tui/gc_tuning.py +63 -0
  613. novacode_cli/tui/handoff_screen.py +206 -0
  614. novacode_cli/tui/harness.py +244 -0
  615. novacode_cli/tui/history_tree_screen.py +179 -0
  616. novacode_cli/tui/home.py +328 -0
  617. novacode_cli/tui/home_scene.py +94 -0
  618. novacode_cli/tui/icons.py +81 -0
  619. novacode_cli/tui/interface_styles.tcss +327 -0
  620. novacode_cli/tui/matrix3.py +108 -0
  621. novacode_cli/tui/mcp_tools_screen.py +131 -0
  622. novacode_cli/tui/motion.py +178 -0
  623. novacode_cli/tui/output_body.py +136 -0
  624. novacode_cli/tui/output_buffer.py +59 -0
  625. novacode_cli/tui/palette.py +334 -0
  626. novacode_cli/tui/pickers.py +237 -0
  627. novacode_cli/tui/prime_panel.py +506 -0
  628. novacode_cli/tui/provider_signin.py +213 -0
  629. novacode_cli/tui/pty_shell.py +159 -0
  630. novacode_cli/tui/reference_screens.py +444 -0
  631. novacode_cli/tui/resume_picker.py +346 -0
  632. novacode_cli/tui/rooms.py +597 -0
  633. novacode_cli/tui/screens.py +7088 -0
  634. novacode_cli/tui/session_import.py +333 -0
  635. novacode_cli/tui/session_pane.py +242 -0
  636. novacode_cli/tui/session_tab.py +42 -0
  637. novacode_cli/tui/session_transcript.py +118 -0
  638. novacode_cli/tui/shell_output.py +7 -0
  639. novacode_cli/tui/stall_watch.py +174 -0
  640. novacode_cli/tui/statusline.py +40 -0
  641. novacode_cli/tui/statusline_screen.py +91 -0
  642. novacode_cli/tui/teammate_updates.py +162 -0
  643. novacode_cli/tui/teammates.py +548 -0
  644. novacode_cli/tui/terminal.py +289 -0
  645. novacode_cli/tui/terminal_view.py +235 -0
  646. novacode_cli/tui/themes.py +118 -0
  647. novacode_cli/tui/update_notice.py +84 -0
  648. novacode_cli/tui/voice_downloads.py +113 -0
  649. novacode_cli/tui/widgets.py +2038 -0
  650. novacode_cli/tui/windows_input.py +38 -0
  651. novacode_cli/tui/workspace_approval.py +165 -0
  652. novacode_cli/ui/council_web.py +113 -0
  653. novacode_cli/ui/council_workspace.py +72 -0
  654. novacode_cli/ui/cowork_web.py +41 -0
  655. novacode_cli/ui/diff_highlight.py +160 -0
  656. novacode_cli/ui/execution.py +1396 -0
  657. novacode_cli/ui/exit_summary.py +65 -0
  658. novacode_cli/ui/hitl_approval.py +196 -0
  659. novacode_cli/ui/input_preparation.py +2 -0
  660. novacode_cli/ui/interrupt_handlers.py +215 -0
  661. novacode_cli/ui/question_prompt.py +151 -0
  662. novacode_cli/ui/status_phrases.py +157 -0
  663. novacode_cli/ui/stream_preview.py +66 -0
  664. novacode_cli/ui/streaming.py +2 -0
  665. novacode_cli/ui/subagent_tracking.py +2 -0
  666. novacode_cli/ui/ui_elements.py +1585 -0
  667. novacode_cli/ui/web_workspace.py +244 -0
  668. novacode_cli/ui_events.py +299 -0
  669. novacode_cli/update_ui.py +243 -0
  670. novacode_cli/updates.py +489 -0
  671. novacode_cli/utils/backend_patches.py +399 -0
  672. novacode_cli/utils/backend_paths.py +467 -0
  673. novacode_cli/utils/lazy_providers.py +51 -0
  674. novacode_cli/utils/model_info.py +223 -0
  675. novacode_cli/utils/pdf_extraction.py +183 -0
  676. novacode_cli/utils/startup.py +29 -0
  677. novacode_cli/utils/tasks.py +42 -0
  678. novacode_cli/utils/windows.py +9 -0
  679. novacode_cli/vixie/hook_adapter.py +127 -0
  680. novacode_cli/vixie/server.py +375 -0
  681. novacode_cli/wiki/__init__.py +15 -0
  682. novacode_cli/wiki/ask.py +56 -0
  683. novacode_cli/wiki/file.py +71 -0
  684. novacode_cli/wiki/ingest.py +184 -0
  685. novacode_cli/wiki/manager.py +261 -0
  686. novacode_cli-1.2.0.dist-info/METADATA +271 -0
  687. novacode_cli-1.2.0.dist-info/RECORD +691 -0
  688. novacode_cli-1.2.0.dist-info/WHEEL +5 -0
  689. novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
  690. novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
  691. 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)