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