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,248 @@
1
+ """Versioned, checksummed, atomically published full-message recovery archives."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import uuid
8
+ from pathlib import Path
9
+
10
+ # JSON and LangGraph interfaces carry heterogeneous values.
11
+ # ruff: noqa: ANN401
12
+ from typing import Any
13
+
14
+ from langchain_core.messages import messages_from_dict, messages_to_dict
15
+
16
+ from novacode_cli.hermes.activation import content_hash, publish
17
+
18
+ MANIFEST_VERSION = 2
19
+
20
+
21
+ class RecoveryArchiveError(ValueError):
22
+ """An archive is incomplete or its checksum does not match."""
23
+
24
+
25
+ class RecoveryStore:
26
+ """Keep immutable archives and identify older incomplete transcript files."""
27
+
28
+ def __init__(self, root: Path) -> None:
29
+ """Initialize local state without acquiring invocation ownership."""
30
+ self.root = Path(root)
31
+
32
+ def _blob(self, data: bytes, blobs: dict) -> Any:
33
+ digest = content_hash(data)
34
+ path = self.root / "blobs" / digest
35
+ if path.exists():
36
+ if path.read_bytes() != data:
37
+ message = "Immutable recovery blob checksum conflict"
38
+ raise RecoveryArchiveError(message)
39
+ else:
40
+ publish(path, data)
41
+ blobs[digest] = len(data)
42
+ return {"__nova_binary__": digest}
43
+
44
+ def _encode(self, value: Any, blobs: Any) -> Any:
45
+ if isinstance(value, bytes):
46
+ return self._blob(value, blobs)
47
+ if isinstance(value, dict):
48
+ if set(value) in ({"__nova_binary__"}, {"__nova_mapping__"}):
49
+ return {
50
+ "__nova_mapping__": [
51
+ [key, self._encode(item, blobs)] for key, item in value.items()
52
+ ]
53
+ }
54
+ return {key: self._encode(item, blobs) for key, item in value.items()}
55
+ if isinstance(value, (tuple, list)):
56
+ return [self._encode(item, blobs) for item in value]
57
+ if value is None or isinstance(value, (str, int, float, bool)):
58
+ return value
59
+ message = f"Unsupported archive value: {type(value).__name__}"
60
+ raise RecoveryArchiveError(message)
61
+
62
+ def _read_blob(self, digest: str) -> bytes:
63
+ if not re.fullmatch(r"[0-9a-f]{64}", digest):
64
+ message = "Invalid blob reference"
65
+ raise RecoveryArchiveError(message)
66
+ data = (self.root / "blobs" / digest).read_bytes()
67
+ if content_hash(data) != digest:
68
+ message = "Recovery blob checksum mismatch"
69
+ raise RecoveryArchiveError(message)
70
+ return data
71
+
72
+ def _decode(self, value: Any) -> Any:
73
+ if isinstance(value, dict):
74
+ if set(value) == {"__nova_mapping__"}:
75
+ return {key: self._decode(item) for key, item in value["__nova_mapping__"]}
76
+ if set(value) == {"__nova_binary__"}:
77
+ return self._read_blob(value["__nova_binary__"])
78
+ return {key: self._decode(item) for key, item in value.items()}
79
+ if isinstance(value, list):
80
+ return [self._decode(item) for item in value]
81
+ return value
82
+
83
+ def archive(
84
+ self,
85
+ thread: str,
86
+ messages: list,
87
+ *,
88
+ approvals: Any = (),
89
+ state: Any = None,
90
+ source: str = "manual",
91
+ checkpoint: Any = None,
92
+ ) -> Path:
93
+ """Publish a complete manifest after all message blobs are durable."""
94
+ blobs: dict[str, int] = {}
95
+ payload = self._encode(
96
+ {
97
+ "messages": messages_to_dict(messages),
98
+ "approvals": approvals,
99
+ "state": state or {},
100
+ "checkpoint": checkpoint,
101
+ },
102
+ blobs,
103
+ )
104
+ encoded = json.dumps(payload, ensure_ascii=False, allow_nan=False).encode()
105
+ digest = content_hash(encoded)
106
+ publish(self.root / "blobs" / digest, encoded)
107
+ manifest = {
108
+ "version": MANIFEST_VERSION,
109
+ "id": uuid.uuid4().hex,
110
+ "thread": thread,
111
+ "source": source,
112
+ "message_count": len(messages),
113
+ "payload": digest,
114
+ "blobs": blobs,
115
+ "complete": True,
116
+ }
117
+ manifest["checksum"] = content_hash(json.dumps(manifest, sort_keys=True).encode())
118
+ destination = self.root / "manifests" / f"{manifest['id']}.json"
119
+ publish(destination, json.dumps(manifest, sort_keys=True).encode())
120
+ return destination
121
+
122
+ def inspect(self, reference: str) -> Any:
123
+ """Validate an archive and all its referenced blobs before returning it."""
124
+ identity = Path(reference).stem
125
+ if not re.fullmatch(r"[0-9a-f]{32}", identity):
126
+ message = "Use a recovery ID from /context recovery"
127
+ raise RecoveryArchiveError(message)
128
+ manifest = json.loads((self.root / "manifests" / f"{identity}.json").read_text())
129
+ if manifest.get("version") not in {1, 2} or not manifest.get("complete"):
130
+ message = "Incomplete or unsupported recovery manifest"
131
+ raise RecoveryArchiveError(message)
132
+ if manifest["version"] == MANIFEST_VERSION:
133
+ checked = dict(manifest)
134
+ checksum = checked.pop("checksum", None)
135
+ if checksum != content_hash(json.dumps(checked, sort_keys=True).encode()):
136
+ message = "Recovery manifest checksum mismatch"
137
+ raise RecoveryArchiveError(message)
138
+ else:
139
+ manifest["integrity"] = "legacy: payload verified; manifest checksum unavailable"
140
+ if not isinstance(manifest.get("blobs"), dict) or not isinstance(
141
+ manifest.get("payload"), str
142
+ ):
143
+ message = "Incomplete recovery manifest references"
144
+ raise RecoveryArchiveError(message)
145
+ payload = self._decode(json.loads(self._read_blob(manifest["payload"])))
146
+ for digest, size in manifest["blobs"].items():
147
+ if len(self._read_blob(digest)) != size:
148
+ message = "Recovery blob length mismatch"
149
+ raise RecoveryArchiveError(message)
150
+ return manifest, payload
151
+
152
+ def list(self, thread: str | None = None) -> Any:
153
+ """List recoverable archives and label damaged references."""
154
+ entries = []
155
+ for path in sorted((self.root / "manifests").glob("*.json")):
156
+ try:
157
+ manifest, _ = self.inspect(path.stem)
158
+ if thread is None or manifest["thread"] == thread:
159
+ entries.append(manifest)
160
+ except (OSError, ValueError, KeyError, TypeError) as error:
161
+ entries.append({"id": path.stem, "complete": False, "error": str(error)})
162
+ sessions = self.root.parent / "sessions"
163
+ if sessions.is_dir():
164
+ for session in sorted(sessions.iterdir()):
165
+ if not session.is_dir() or (thread is not None and session.name != thread):
166
+ continue
167
+ for pattern, source in (
168
+ ("pre-compact-*.jsonl", "manual"),
169
+ ("conversation_history/*.md", "automatic"),
170
+ ):
171
+ entries.extend(
172
+ {
173
+ "id": f"legacy:{legacy.relative_to(sessions).as_posix()}",
174
+ "thread": session.name,
175
+ "source": source,
176
+ "path": str(legacy),
177
+ "complete": False,
178
+ "restorable": False,
179
+ "error": "Legacy transcript lacks a complete checkpoint manifest",
180
+ }
181
+ for legacy in sorted(session.glob(pattern))
182
+ if legacy.is_file()
183
+ )
184
+ return entries
185
+
186
+ async def restore(self, reference: str, agent: Any) -> str:
187
+ """Restore messages and graph approvals into a fresh checkpoint thread."""
188
+ import asyncio
189
+
190
+ _manifest, payload = await asyncio.to_thread(self.inspect, reference)
191
+ thread = uuid.uuid4().hex
192
+ config = {"configurable": {"thread_id": thread, "checkpoint_ns": ""}}
193
+ state = dict(payload["state"])
194
+ state["messages"] = messages_from_dict(payload["messages"])
195
+ # Persist approval interrupts through the saver, rather than advancing
196
+ # the graph's model node and losing the pending task/approval structure.
197
+ if payload.get("checkpoint"):
198
+ from novacode_cli.session.checkpoint_coordinator import CoordinatedGraph
199
+
200
+ if not isinstance(agent, CoordinatedGraph):
201
+ message = "Checkpoint restoration requires a coordinated graph"
202
+ raise RecoveryArchiveError(message)
203
+ saver = agent.coordinator.saver
204
+ packed = payload["checkpoint"]
205
+ checkpoint = saver.serde.loads_typed(tuple(packed["checkpoint"]))
206
+ pending = saver.serde.loads_typed(tuple(packed["pending"]))
207
+ async with agent.coordinator.mutation(config, agent.project):
208
+ saved = await saver.aput(
209
+ config, checkpoint, packed["metadata"], checkpoint["channel_versions"]
210
+ )
211
+ grouped: dict[str, list[tuple[str, Any]]] = {}
212
+ for task_id, channel, value in pending:
213
+ grouped.setdefault(task_id, []).append((channel, value))
214
+ for task_id, writes in grouped.items():
215
+ await saver.aput_writes(saved, writes, task_id)
216
+ return thread
217
+ if payload["approvals"]:
218
+ message = "Archive contains pending approvals; use checkpoint-preserving restore"
219
+ raise RecoveryArchiveError(message)
220
+ await agent.aupdate_state(config, state)
221
+ return thread
222
+
223
+
224
+ def default_recovery_store() -> RecoveryStore:
225
+ """Resolve the shared local archive store."""
226
+ from novacode_cli.config import config
227
+
228
+ return RecoveryStore(config.HOME_DIR / "recovery")
229
+
230
+
231
+ async def handle_recovery_command(args: str, *, agent: Any, thread: str) -> tuple[str, str | None]:
232
+ """Transport-independent listing, inspection, and fresh-thread restoration."""
233
+ import asyncio
234
+
235
+ parts = args.split()
236
+ store = default_recovery_store()
237
+ if not parts or parts == ["list"]:
238
+ entries = await asyncio.to_thread(store.list, thread)
239
+ return json.dumps(entries, indent=2), None
240
+ command_arity = 2
241
+ if len(parts) == command_arity and parts[0] == "inspect":
242
+ manifest, _payload = await asyncio.to_thread(store.inspect, parts[1])
243
+ return json.dumps(manifest, indent=2), None
244
+ if len(parts) == command_arity and parts[0] == "restore":
245
+ restored = await store.restore(parts[1], agent)
246
+ return f"Restored into new thread {restored}", restored
247
+ message = "Usage: /context recovery [list|inspect <id>|restore <id>]"
248
+ raise ValueError(message)
@@ -0,0 +1,65 @@
1
+ """Separate text estimates, fixed image heuristics and image transport size."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from langchain_core.messages.utils import count_tokens_approximately
10
+
11
+ if TYPE_CHECKING:
12
+ from langchain_core.messages import AnyMessage
13
+
14
+ IMAGE_TYPES = frozenset({"image", "image_url", "input_image"})
15
+ TOKENS_PER_IMAGE = 85
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ContentEstimate:
20
+ """Heuristics only; image dimensions and provider billing are unobserved."""
21
+
22
+ text_tokens: int
23
+ image_tokens: int
24
+ image_transport_chars: int
25
+
26
+ @property
27
+ def total(self) -> int:
28
+ """Input estimate excluding schemas, with no base64 character charge."""
29
+ return self.text_tokens + self.image_tokens
30
+
31
+
32
+ def estimate_content(messages: Sequence[AnyMessage]) -> ContentEstimate:
33
+ """Count images separately, including nested tool-result image blocks."""
34
+ images = transport = 0
35
+
36
+ def sanitize(value: Any) -> Any: # noqa: ANN401 - heterogeneous content blocks
37
+ nonlocal images, transport
38
+ if isinstance(value, list):
39
+ return [sanitize(item) for item in value]
40
+ if not isinstance(value, Mapping):
41
+ return value
42
+ if value.get("type") in IMAGE_TYPES:
43
+ images += 1
44
+ payload = value.get("base64") or value.get("image_url") or value.get("url")
45
+ if isinstance(payload, Mapping):
46
+ payload = payload.get("url") or payload.get("data")
47
+ source = value.get("source")
48
+ if not payload and isinstance(source, Mapping):
49
+ payload = source.get("data")
50
+ if isinstance(payload, str):
51
+ if payload.startswith("data:"):
52
+ transport += len(payload.partition(",")[2])
53
+ elif not payload.startswith(("https://", "http://")):
54
+ transport += len(payload)
55
+ return {"type": "image"}
56
+ return {key: sanitize(item) for key, item in value.items()}
57
+
58
+ sanitized = [
59
+ message.model_copy(update={"content": sanitize(message.content)})
60
+ if isinstance(message.content, list)
61
+ else message
62
+ for message in messages
63
+ ]
64
+ text_tokens = count_tokens_approximately(sanitized, tokens_per_image=0)
65
+ return ContentEstimate(text_tokens, images * TOKENS_PER_IMAGE, transport)
@@ -0,0 +1,17 @@
1
+ """Reconstruct the conversation sent to the model after library compaction."""
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+
7
+ def effective_messages(values: Mapping[str, Any]) -> list[Any]:
8
+ """Use the summary and retained tail instead of the checkpoint archive."""
9
+ messages = list(values.get("messages", []))
10
+ event = values.get("_summarization_event")
11
+ if not isinstance(event, Mapping):
12
+ return messages
13
+ cutoff = event.get("cutoff_index")
14
+ summary = event.get("summary_message")
15
+ if not isinstance(cutoff, int) or cutoff < 0 or summary is None:
16
+ return messages
17
+ return [summary, *messages[cutoff:]]
@@ -0,0 +1,292 @@
1
+ """Disposable, owner-private SQLite knowledge artifacts and producer fences."""
2
+ # ruff: noqa: ANN401, EM101, TRY003
3
+
4
+ from __future__ import annotations
5
+
6
+ import csv
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import sqlite3
11
+ import subprocess
12
+ import time
13
+ from contextlib import contextmanager
14
+ from pathlib import Path
15
+ from threading import RLock
16
+ from typing import Any
17
+
18
+ TTL = 7 * 24 * 3600
19
+ MAX_ARTIFACTS = 2000
20
+ MAX_PAYLOAD = 64 * 1024 * 1024
21
+ LEASE_SECONDS = 90
22
+
23
+
24
+ def digest(value: str | bytes) -> str:
25
+ """Hash identities or complete source bytes without storing the original."""
26
+ return hashlib.sha256(value.encode() if isinstance(value, str) else value).hexdigest()
27
+
28
+
29
+ def _protect(directory: Path) -> None:
30
+ """Restrict the directory before creating SQLite and its WAL sidecars."""
31
+ directory.mkdir(parents=True, exist_ok=True, mode=0o700)
32
+ if directory.is_symlink() or directory.resolve() != directory.absolute():
33
+ raise ValueError("Knowledge directory must not contain links")
34
+ if os.name != "nt":
35
+ directory.chmod(0o700)
36
+ return
37
+ executable = Path(os.environ.get("SYSTEMROOT", "C:/Windows")) / "System32/icacls.exe"
38
+ identity = subprocess.run( # noqa: S603
39
+ [str(executable.with_name("whoami.exe")), "/user", "/fo", "csv", "/nh"],
40
+ capture_output=True,
41
+ text=True,
42
+ timeout=10,
43
+ check=True,
44
+ creationflags=subprocess.CREATE_NO_WINDOW,
45
+ )
46
+ sid = next(csv.reader([identity.stdout.strip()]))[1]
47
+ if not sid.startswith("S-1-") or any(c not in "S0123456789-" for c in sid):
48
+ raise OSError("Could not determine knowledge directory owner")
49
+ subprocess.run( # noqa: S603
50
+ [str(executable), str(directory), "/inheritance:r", "/grant:r", f"*{sid}:(OI)(CI)F"],
51
+ stdout=subprocess.DEVNULL,
52
+ stderr=subprocess.DEVNULL,
53
+ timeout=10,
54
+ check=True,
55
+ creationflags=subprocess.CREATE_NO_WINDOW,
56
+ )
57
+
58
+
59
+ class KnowledgeCache:
60
+ """Short independent transactions; never hold a connection across model work."""
61
+
62
+ def __init__(self, directory: Path) -> None:
63
+ """Defer filesystem and database work until a lookup or command."""
64
+ self.directory = directory.absolute()
65
+ self._ready = False
66
+ self._lock = RLock()
67
+
68
+ @contextmanager
69
+ def connection(self) -> Any:
70
+ """Serialize lazy initialization and short writes within this instance."""
71
+ with self._lock, self._connection() as db:
72
+ yield db
73
+
74
+ @contextmanager
75
+ def _connection(self) -> Any:
76
+ """Open a bounded writer transaction on a private disposable database."""
77
+ if not self._ready:
78
+ _protect(self.directory)
79
+ path = self.directory / "knowledge.sqlite3"
80
+ if path.is_symlink():
81
+ raise ValueError("Knowledge database must not be a link")
82
+ db = sqlite3.connect(path, timeout=0.5)
83
+ db.row_factory = sqlite3.Row
84
+ try:
85
+ if not self._ready:
86
+ self._wal(db)
87
+ db.executescript("""
88
+ CREATE TABLE IF NOT EXISTS namespaces (
89
+ ns TEXT PRIMARY KEY, epoch INTEGER NOT NULL DEFAULT 0,
90
+ semantic_cache_enabled INTEGER NOT NULL DEFAULT 1,
91
+ stats TEXT NOT NULL DEFAULT '{}');
92
+ CREATE TABLE IF NOT EXISTS artifacts (
93
+ ns TEXT NOT NULL, key TEXT NOT NULL, refs TEXT NOT NULL,
94
+ summary TEXT NOT NULL, model TEXT NOT NULL,
95
+ embedding TEXT NOT NULL, vector TEXT, created REAL NOT NULL,
96
+ expires REAL NOT NULL, accessed REAL NOT NULL, bytes INTEGER NOT NULL,
97
+ PRIMARY KEY(ns,key));
98
+ CREATE TABLE IF NOT EXISTS leases (
99
+ ns TEXT NOT NULL, key TEXT NOT NULL, epoch INTEGER NOT NULL,
100
+ owner TEXT NOT NULL, until REAL NOT NULL, PRIMARY KEY(ns,key));
101
+ CREATE INDEX IF NOT EXISTS artifact_access ON artifacts(accessed);
102
+ """)
103
+ self._ready = True
104
+ db.execute("BEGIN IMMEDIATE")
105
+ yield db
106
+ db.commit()
107
+ except BaseException:
108
+ db.rollback()
109
+ raise
110
+ finally:
111
+ db.close()
112
+
113
+ @staticmethod
114
+ def _wal(db: sqlite3.Connection) -> None:
115
+ """Bound retries for SQLite's non-waiting initial journal-mode upgrade."""
116
+ deadline = time.monotonic() + 0.5
117
+ while True:
118
+ try:
119
+ db.execute("PRAGMA journal_mode=WAL")
120
+ return # noqa: TRY300
121
+ except sqlite3.OperationalError as exc:
122
+ if (
123
+ getattr(exc, "sqlite_errorcode", 0)
124
+ not in {sqlite3.SQLITE_BUSY, sqlite3.SQLITE_LOCKED}
125
+ or time.monotonic() >= deadline
126
+ ):
127
+ raise
128
+ time.sleep(0.025)
129
+
130
+ @staticmethod
131
+ def namespace(root: Path, assistant: str) -> str:
132
+ """Canonical workspace plus assistant identity is the sharing boundary."""
133
+ return digest(json.dumps([os.path.normcase(str(root.resolve())), assistant]))
134
+
135
+ @staticmethod
136
+ def _state(db: sqlite3.Connection, ns: str) -> sqlite3.Row:
137
+ db.execute("INSERT OR IGNORE INTO namespaces(ns) VALUES (?)", (ns,))
138
+ return db.execute("SELECT * FROM namespaces WHERE ns=?", (ns,)).fetchone()
139
+
140
+ def state(self, ns: str) -> dict:
141
+ """Read the immediately authoritative enable flag and epoch."""
142
+ with self.connection() as db:
143
+ return dict(self._state(db, ns))
144
+
145
+ def control(self, ns: str, action: str) -> dict:
146
+ """Clear/off fence old producers; each namespace retains its own toggle."""
147
+ with self.connection() as db:
148
+ self._state(db, ns)
149
+ if action in {"clear", "off"}:
150
+ db.execute("UPDATE namespaces SET epoch=epoch+1 WHERE ns=?", (ns,))
151
+ db.execute("DELETE FROM artifacts WHERE ns=?", (ns,))
152
+ db.execute("DELETE FROM leases WHERE ns=?", (ns,))
153
+ if action in {"on", "off"}:
154
+ db.execute(
155
+ "UPDATE namespaces SET semantic_cache_enabled=? WHERE ns=?",
156
+ (int(action == "on"), ns),
157
+ )
158
+ self._evict(db, ns)
159
+ state = self._state(db, ns)
160
+ stats = json.loads(state["stats"])
161
+ retained = db.execute(
162
+ "SELECT coalesce(sum(bytes),0) FROM artifacts WHERE ns=?", (ns,)
163
+ ).fetchone()[0]
164
+ return {
165
+ "semantic_cache_enabled": bool(state["semantic_cache_enabled"]),
166
+ "epoch": state["epoch"],
167
+ "retained_bytes": retained,
168
+ **stats,
169
+ }
170
+
171
+ def event(self, ns: str, name: str, value: float = 1) -> None:
172
+ """Persist only numeric accounting, never queries or source content."""
173
+ with self.connection() as db:
174
+ stats = json.loads(self._state(db, ns)["stats"])
175
+ stats[name] = stats.get(name, 0) + value
176
+ db.execute("UPDATE namespaces SET stats=? WHERE ns=?", (json.dumps(stats), ns))
177
+
178
+ def candidates(self, ns: str, model: str, key: str | None) -> list[dict]:
179
+ """Retrieve bounded metadata; callers must validate every returned source."""
180
+ with self.connection() as db:
181
+ self._evict(db, ns)
182
+ rows = db.execute(
183
+ "SELECT * FROM artifacts WHERE ns=? AND model=?" + (" AND key=?" if key else ""), # noqa: S608 - fixed SQL suffix.
184
+ (ns, model, key) if key else (ns, model),
185
+ ).fetchall()
186
+ return [dict(row) for row in rows]
187
+
188
+ def touch(self, ns: str, key: str, epoch: int) -> bool:
189
+ """Validate the fence and update access after source validation."""
190
+ with self.connection() as db:
191
+ state = self._state(db, ns)
192
+ if state["epoch"] != epoch or not state["semantic_cache_enabled"]:
193
+ return False
194
+ return bool(
195
+ db.execute(
196
+ "UPDATE artifacts SET accessed=? WHERE ns=? AND key=?",
197
+ (self._access(db), ns, key),
198
+ ).rowcount
199
+ )
200
+
201
+ def invalidate(self, ns: str, key: str) -> None:
202
+ """Discard a source-invalid artifact."""
203
+ with self.connection() as db:
204
+ db.execute("DELETE FROM artifacts WHERE ns=? AND key=?", (ns, key))
205
+ self.event(ns, "invalidations")
206
+
207
+ def acquire(self, ns: str, key: str, epoch: int, owner: str) -> bool:
208
+ """Lease a single producer across processes, expiring crashed producers."""
209
+ with self.connection() as db:
210
+ state = self._state(db, ns)
211
+ if state["epoch"] != epoch or not state["semantic_cache_enabled"]:
212
+ return False
213
+ db.execute("DELETE FROM leases WHERE until<=?", (time.time(),))
214
+ return bool(
215
+ db.execute(
216
+ "INSERT OR IGNORE INTO leases VALUES (?,?,?,?,?)",
217
+ (ns, key, epoch, owner, time.time() + LEASE_SECONDS),
218
+ ).rowcount
219
+ )
220
+
221
+ def release(self, ns: str, key: str, owner: str) -> None:
222
+ """Release only this producer's lease, never a replacement lease."""
223
+ with self.connection() as db:
224
+ db.execute("DELETE FROM leases WHERE ns=? AND key=? AND owner=?", (ns, key, owner))
225
+
226
+ def publish(
227
+ self,
228
+ ns: str,
229
+ key: str,
230
+ epoch: int,
231
+ owner: str,
232
+ refs: str,
233
+ summary: str,
234
+ model: str,
235
+ embedding: str,
236
+ vector: list[float] | None,
237
+ ) -> bool:
238
+ """Commit only while both the namespace fence and producer lease match."""
239
+ payload = json.dumps(vector, allow_nan=False) if vector is not None else None
240
+ size = len((refs + summary + model + embedding + (payload or "")).encode())
241
+ with self.connection() as db:
242
+ state = self._state(db, ns)
243
+ lease = db.execute("SELECT * FROM leases WHERE ns=? AND key=?", (ns, key)).fetchone()
244
+ if (
245
+ state["epoch"] != epoch
246
+ or not state["semantic_cache_enabled"]
247
+ or not lease
248
+ or lease["owner"] != owner
249
+ or lease["until"] <= time.time()
250
+ ):
251
+ return False
252
+ now = time.time()
253
+ db.execute(
254
+ "INSERT OR REPLACE INTO artifacts VALUES (?,?,?,?,?,?,?,?,?,?,?)",
255
+ (
256
+ ns,
257
+ key,
258
+ refs,
259
+ summary,
260
+ model,
261
+ embedding,
262
+ payload,
263
+ now,
264
+ now + TTL,
265
+ self._access(db),
266
+ size,
267
+ ),
268
+ )
269
+ self._evict(db, ns)
270
+ return True
271
+
272
+ @staticmethod
273
+ def _access(db: sqlite3.Connection) -> int:
274
+ """Order touches even when the platform clock repeats or moves backwards."""
275
+ previous = db.execute("SELECT coalesce(max(accessed),0) FROM artifacts").fetchone()[0]
276
+ return max(time.time_ns() // 1000, int(previous) + 1)
277
+
278
+ @staticmethod
279
+ def _evict(db: sqlite3.Connection, ns: str) -> None:
280
+ db.execute("DELETE FROM artifacts WHERE expires<=?", (time.time(),))
281
+ db.execute(
282
+ "DELETE FROM artifacts WHERE ns=? AND key IN "
283
+ "(SELECT key FROM artifacts WHERE ns=? ORDER BY accessed DESC LIMIT -1 OFFSET ?)",
284
+ (ns, ns, MAX_ARTIFACTS),
285
+ )
286
+ total = db.execute("SELECT coalesce(sum(bytes),0) FROM artifacts").fetchone()[0]
287
+ while total > MAX_PAYLOAD:
288
+ row = db.execute(
289
+ "SELECT ns,key,bytes FROM artifacts ORDER BY accessed LIMIT 1"
290
+ ).fetchone()
291
+ db.execute("DELETE FROM artifacts WHERE ns=? AND key=?", (row["ns"], row["key"]))
292
+ total -= row["bytes"]