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