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,1887 @@
1
+ """HTTP server, REST API, and self-contained HTML page for the Skills & Agents web UI.
2
+
3
+ The CreateServer class starts a lightweight HTTP server (stdlib http.server)
4
+ in a background daemon thread, serving a single-page app with two tabs:
5
+ Skills and Agents. Users can browse, preview, edit, create, and delete
6
+ skills (SKILL.md) and agents (agent.md) across global and project scopes.
7
+
8
+ REST API endpoints:
9
+ GET /api/skills — list all skills
10
+ GET /api/skills/<name> — get SKILL.md content
11
+ PUT /api/skills/<name> — update SKILL.md content
12
+ POST /api/skills — create a new skill
13
+ DELETE /api/skills/<name> — delete a skill
14
+ GET /api/agents — list all agents
15
+ GET /api/agents/<name> — get agent.md content
16
+ PUT /api/agents/<name> — update agent.md content
17
+ POST /api/agents — create a new agent
18
+ DELETE /api/agents/<name> — delete an agent
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import re
26
+ import socket
27
+ import threading
28
+ from http.server import HTTPServer
29
+ from typing import TYPE_CHECKING, Any
30
+
31
+ from novacode_cli.config.config import Settings, agent_md
32
+
33
+ if TYPE_CHECKING:
34
+ from pathlib import Path
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Helpers
38
+ # ---------------------------------------------------------------------------
39
+
40
+ def _extract_frontmatter_field(content: str, field: str) -> str | None:
41
+ """Extract a YAML frontmatter field value from markdown content."""
42
+ if not content.startswith("---"):
43
+ return None
44
+ parts = content.split("---", 2)
45
+ if len(parts) < 3:
46
+ return None
47
+ frontmatter = parts[1]
48
+ for line in frontmatter.splitlines():
49
+ line = line.strip()
50
+ if line.startswith(f"{field}:"):
51
+ return line.split(":", 1)[1].strip().strip('"').strip("'")
52
+ return None
53
+
54
+
55
+ def _ensure_frontmatter(content: str, name: str, description: str | None = None) -> str:
56
+ """Ensure content has YAML frontmatter with at least name and description."""
57
+ if content.startswith("---"):
58
+ # Already has frontmatter — ensure name field
59
+ parts = content.split("---", 2)
60
+ if len(parts) >= 3:
61
+ frontmatter = parts[1]
62
+ body = parts[2]
63
+ if not re.search(r"^name:\s*", frontmatter, re.MULTILINE):
64
+ frontmatter = f"name: {name}\n{frontmatter}"
65
+ if description and not re.search(r"^description:\s*", frontmatter, re.MULTILINE):
66
+ frontmatter = f"{frontmatter}description: {description}\n"
67
+ return f"---{frontmatter}---{body}"
68
+ # No frontmatter — add one
69
+ desc_line = f"\ndescription: {description}" if description else ""
70
+ return f"---\nname: {name}{desc_line}\n---\n\n{content}"
71
+
72
+
73
+ def _is_valid_name(name: str) -> bool:
74
+ """Validate a skill or agent name (alphanumeric, hyphens, underscores)."""
75
+ return bool(name and re.match(r"^[a-zA-Z0-9_\-]+$", name))
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # HTML Page
80
+ # ---------------------------------------------------------------------------
81
+
82
+ HTML_PAGE = r"""<!DOCTYPE html>
83
+ <html lang="en">
84
+ <head>
85
+ <meta charset="UTF-8">
86
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
87
+ <title>Nova Create — Skills & Agents</title>
88
+ <link rel="preconnect" href="https://fonts.googleapis.com">
89
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
90
+ <link href="https://fonts.googleapis.com/css2?family=Syne:wght@500;600;700;800&family=DM+Sans:ital,opsz,wght@0,9..40,300..700;1,9..40,300..700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
91
+ <style>
92
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
93
+
94
+ :root {
95
+ /* ── Nova TUI (tokyo-night) palette ─────────────────────── */
96
+ --bg-deep: #13141d; /* background */
97
+ --bg-surface: #1a1b26; /* surface */
98
+ --bg-elevated: #24283b; /* panel */
99
+ --bg-hover: #2f3346; /* boost */
100
+ --border-subtle: #3b4261; /* border */
101
+ --border-mid: #565f89; /* text-muted */
102
+ --text-primary: #c0caf5; /* foreground */
103
+ --text-secondary: #a9b1d6;
104
+ --text-muted: #565f89;
105
+ --primary: #7aa2f7; /* blue */
106
+ --primary-dim: rgba(122, 162, 247, 0.12);
107
+ --primary-glow: rgba(122, 162, 247, 0.25);
108
+ --accent: #bb9af7; /* purple */
109
+ --accent-dim: rgba(187, 154, 247, 0.12);
110
+ --accent-glow: rgba(187, 154, 247, 0.2);
111
+ --secondary: #9ece6a; /* green */
112
+ --success: #73daca; /* teal */
113
+ --success-dim: rgba(115, 218, 202, 0.12);
114
+ --success-glow: rgba(115, 218, 202, 0.2);
115
+ --warning: #e0af68; /* orange */
116
+ --error: #f7768e; /* red */
117
+ --error-dim: rgba(247, 118, 142, 0.12);
118
+ --radius: 14px;
119
+ --radius-sm: 8px;
120
+ --radius-xs: 6px;
121
+ }
122
+
123
+ html, body { height: 100%; }
124
+
125
+ body {
126
+ font-family: 'JetBrains Mono', 'DM Sans', system-ui, sans-serif;
127
+ background: var(--bg-deep);
128
+ color: var(--text-primary);
129
+ display: flex;
130
+ flex-direction: column;
131
+ position: relative;
132
+ overflow: hidden;
133
+ }
134
+
135
+ /* ── Background: tokyo-night navy with subtle blue/purple flares ── */
136
+
137
+ body::before {
138
+ content: '';
139
+ position: fixed;
140
+ inset: 0;
141
+ background:
142
+ radial-gradient(ellipse 90% 50% at 55% -10%, rgba(122, 162, 247, 0.08), transparent 70%),
143
+ radial-gradient(ellipse 60% 40% at 30% 90%, rgba(115, 218, 202, 0.05), transparent 60%),
144
+ radial-gradient(ellipse 50% 50% at 80% 20%, rgba(187, 154, 247, 0.05), transparent 50%);
145
+ pointer-events: none;
146
+ z-index: 0;
147
+ }
148
+
149
+ /* Matrix-rain grid — the TUI's signature, rendered as a faint falling grid */
150
+ body::after {
151
+ content: '';
152
+ position: fixed;
153
+ inset: 0;
154
+ background-image:
155
+ linear-gradient(rgba(122, 162, 247, 0.03) 1px, transparent 1px),
156
+ linear-gradient(90deg, rgba(122, 162, 247, 0.03) 1px, transparent 1px);
157
+ background-size: 48px 48px;
158
+ pointer-events: none;
159
+ z-index: 0;
160
+ }
161
+
162
+ .grain {
163
+ position: fixed;
164
+ inset: 0;
165
+ pointer-events: none;
166
+ z-index: 1;
167
+ opacity: 0.03;
168
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
169
+ }
170
+
171
+ /* ── Floating ambient particles (tinted tokyo-night) ──────── */
172
+
173
+ .particle-field {
174
+ position: fixed;
175
+ inset: 0;
176
+ pointer-events: none;
177
+ z-index: 1;
178
+ overflow: hidden;
179
+ }
180
+
181
+ .particle {
182
+ position: absolute;
183
+ border-radius: 50%;
184
+ opacity: 0;
185
+ animation: particle-float 12s ease-in-out infinite;
186
+ }
187
+
188
+ .particle:nth-child(1) { width: 3px; height: 3px; background: var(--primary); left: 12%; top: 20%; animation-delay: 0s; animation-duration: 14s; }
189
+ .particle:nth-child(2) { width: 2px; height: 2px; background: var(--success); left: 28%; top: 65%; animation-delay: 2s; animation-duration: 11s; }
190
+ .particle:nth-child(3) { width: 4px; height: 4px; background: var(--accent); left: 55%; top: 15%; animation-delay: 4s; animation-duration: 16s; }
191
+ .particle:nth-child(4) { width: 2px; height: 2px; background: var(--primary); left: 72%; top: 50%; animation-delay: 1s; animation-duration: 13s; }
192
+ .particle:nth-child(5) { width: 3px; height: 3px; background: var(--success); left: 88%; top: 30%; animation-delay: 3s; animation-duration: 15s; }
193
+ .particle:nth-child(6) { width: 2px; height: 2px; background: var(--accent); left: 42%; top: 80%; animation-delay: 5s; animation-duration: 12s; }
194
+
195
+ @keyframes particle-float {
196
+ 0% { opacity: 0; transform: translateY(0) scale(0); }
197
+ 10% { opacity: 0.8; transform: translateY(-10px) scale(1); }
198
+ 40% { opacity: 0.6; transform: translateY(-30px) scale(0.9); }
199
+ 70% { opacity: 0.3; transform: translateY(-50px) scale(0.6); }
200
+ 100% { opacity: 0; transform: translateY(-80px) scale(0); }
201
+ }
202
+
203
+ /* ── Header ─────────────────────────────────────────── */
204
+
205
+ header {
206
+ position: relative;
207
+ z-index: 2;
208
+ padding: 18px 32px;
209
+ display: flex;
210
+ align-items: center;
211
+ justify-content: space-between;
212
+ flex-shrink: 0;
213
+ border-bottom: 1px solid var(--border-subtle);
214
+ background: rgba(19, 20, 29, 0.7);
215
+ backdrop-filter: blur(12px);
216
+ }
217
+
218
+ .header-left { display: flex; align-items: center; gap: 16px; }
219
+
220
+ .logo-icon {
221
+ width: 38px; height: 38px;
222
+ border-radius: 12px;
223
+ background: linear-gradient(145deg, var(--accent), var(--primary));
224
+ display: flex; align-items: center; justify-content: center;
225
+ font-family: 'JetBrains Mono', monospace;
226
+ font-weight: 800; font-size: 20px; color: #13141d;
227
+ box-shadow: 0 0 20px var(--primary-glow);
228
+ position: relative;
229
+ }
230
+
231
+ .logo-icon::after {
232
+ content: '';
233
+ position: absolute;
234
+ inset: -2px;
235
+ border-radius: 14px;
236
+ background: linear-gradient(145deg, var(--primary-glow), transparent);
237
+ z-index: -1;
238
+ }
239
+
240
+ header h1 {
241
+ font-family: 'JetBrains Mono', monospace;
242
+ font-size: 20px;
243
+ font-weight: 700;
244
+ letter-spacing: -0.3px;
245
+ color: var(--text-primary);
246
+ position: relative;
247
+ }
248
+
249
+ header h1::after {
250
+ content: '▮';
251
+ font-size: 10px;
252
+ margin-left: 8px;
253
+ color: var(--primary);
254
+ vertical-align: super;
255
+ opacity: 0.6;
256
+ }
257
+
258
+ header .subtitle {
259
+ font-size: 12px;
260
+ color: var(--text-muted);
261
+ font-weight: 400;
262
+ letter-spacing: 0.2px;
263
+ }
264
+
265
+ .header-right { display: flex; align-items: center; gap: 12px; }
266
+
267
+ .server-badge {
268
+ display: flex; align-items: center; gap: 7px;
269
+ font-family: 'JetBrains Mono', monospace;
270
+ font-size: 10px; font-weight: 500;
271
+ padding: 5px 12px;
272
+ border-radius: 20px;
273
+ background: var(--success-dim);
274
+ color: var(--success);
275
+ border: 1px solid rgba(115, 218, 202, 0.15);
276
+ letter-spacing: 0.5px;
277
+ text-transform: uppercase;
278
+ position: relative;
279
+ overflow: hidden;
280
+ }
281
+
282
+ .server-badge::before {
283
+ content: '';
284
+ position: absolute;
285
+ inset: 0;
286
+ background: linear-gradient(90deg, transparent, rgba(115, 218, 202, 0.05), transparent);
287
+ animation: badge-shimmer 3s ease-in-out infinite;
288
+ }
289
+
290
+ @keyframes badge-shimmer {
291
+ 0%, 100% { transform: translateX(-100%); }
292
+ 50% { transform: translateX(100%); }
293
+ }
294
+
295
+ .live-dot {
296
+ width: 6px; height: 6px;
297
+ border-radius: 50%;
298
+ background: var(--success);
299
+ box-shadow: 0 0 8px var(--success-glow);
300
+ animation: pulse-dot 1.5s ease-in-out infinite;
301
+ }
302
+
303
+ @keyframes pulse-dot {
304
+ 0%, 100% { opacity: 1; transform: scale(1); }
305
+ 50% { opacity: 0.4; transform: scale(0.7); }
306
+ }
307
+
308
+ /* ── Layout ─────────────────────────────────────────── */
309
+
310
+ .app-container {
311
+ position: relative;
312
+ z-index: 2;
313
+ display: flex;
314
+ flex: 1;
315
+ min-height: 0;
316
+ }
317
+
318
+ /* ── Sidebar ─────────────────────────────────────────── */
319
+
320
+ .sidebar {
321
+ width: 300px;
322
+ min-width: 300px;
323
+ display: flex;
324
+ flex-direction: column;
325
+ background: rgba(26, 27, 38, 0.6);
326
+ backdrop-filter: blur(4px);
327
+ border-right: 1px solid var(--border-subtle);
328
+ }
329
+
330
+ .tab-bar {
331
+ display: flex;
332
+ gap: 2px;
333
+ padding: 10px 10px 0;
334
+ flex-shrink: 0;
335
+ }
336
+
337
+ .tab-btn {
338
+ flex: 1;
339
+ padding: 10px 12px;
340
+ border: none;
341
+ border-radius: var(--radius-xs) var(--radius-xs) 0 0;
342
+ background: transparent;
343
+ color: var(--text-muted);
344
+ font-family: 'JetBrains Mono', monospace;
345
+ font-size: 12px;
346
+ font-weight: 600;
347
+ cursor: pointer;
348
+ transition: all 0.25s ease;
349
+ letter-spacing: 0.4px;
350
+ text-transform: uppercase;
351
+ position: relative;
352
+ }
353
+
354
+ .tab-btn:hover { color: var(--text-secondary); background: rgba(36, 40, 59, 0.5); }
355
+ .tab-btn.active {
356
+ color: var(--primary);
357
+ background: rgba(36, 40, 59, 0.8);
358
+ }
359
+ .tab-btn.active::after {
360
+ content: '';
361
+ position: absolute;
362
+ bottom: 0; left: 15%; right: 15%;
363
+ height: 2px;
364
+ background: linear-gradient(90deg, transparent, var(--primary), transparent);
365
+ border-radius: 2px;
366
+ }
367
+
368
+ .tab-btn .count {
369
+ display: inline-block;
370
+ margin-left: 5px;
371
+ font-family: 'JetBrains Mono', monospace;
372
+ font-size: 9px;
373
+ padding: 1px 6px;
374
+ border-radius: 8px;
375
+ background: var(--bg-elevated);
376
+ color: var(--text-muted);
377
+ }
378
+
379
+ .tab-btn.active .count {
380
+ background: var(--primary-dim);
381
+ color: var(--primary);
382
+ }
383
+
384
+ .sidebar-search {
385
+ padding: 8px 10px;
386
+ flex-shrink: 0;
387
+ }
388
+
389
+ .sidebar-search input {
390
+ width: 100%;
391
+ padding: 8px 12px;
392
+ border: 1px solid var(--border-subtle);
393
+ border-radius: var(--radius-xs);
394
+ background: var(--bg-deep);
395
+ color: var(--text-secondary);
396
+ font-family: 'JetBrains Mono', monospace;
397
+ font-size: 12px;
398
+ outline: none;
399
+ transition: all 0.2s ease;
400
+ }
401
+
402
+ .sidebar-search input:focus {
403
+ border-color: var(--primary);
404
+ box-shadow: 0 0 0 2px var(--primary-glow);
405
+ color: var(--text-primary);
406
+ }
407
+
408
+ .sidebar-search input::placeholder { color: var(--text-muted); }
409
+
410
+ .item-list {
411
+ flex: 1;
412
+ overflow-y: auto;
413
+ padding: 6px 8px 8px;
414
+ }
415
+
416
+ .item-list::-webkit-scrollbar { width: 3px; }
417
+ .item-list::-webkit-scrollbar-track { background: transparent; }
418
+ .item-list::-webkit-scrollbar-thumb { background: var(--border-mid); border-radius: 3px; }
419
+
420
+ /* Accent-bar cards — the TUI's signature message-card style */
421
+ .item-card {
422
+ padding: 10px 12px;
423
+ margin-bottom: 3px;
424
+ border-radius: var(--radius-xs);
425
+ cursor: pointer;
426
+ transition: all 0.2s ease;
427
+ border: 1px solid transparent;
428
+ border-left: 3px solid var(--border-subtle);
429
+ background: var(--bg-surface);
430
+ opacity: 0;
431
+ animation: card-slide-in 0.3s ease forwards;
432
+ }
433
+
434
+ @keyframes card-slide-in {
435
+ from { opacity: 0; transform: translateX(-6px); }
436
+ to { opacity: 1; transform: translateX(0); }
437
+ }
438
+
439
+ .item-card:nth-child(1) { animation-delay: 0.02s; }
440
+ .item-card:nth-child(2) { animation-delay: 0.04s; }
441
+ .item-card:nth-child(3) { animation-delay: 0.06s; }
442
+ .item-card:nth-child(4) { animation-delay: 0.08s; }
443
+ .item-card:nth-child(5) { animation-delay: 0.10s; }
444
+ .item-card:nth-child(n+6) { animation-delay: 0.12s; }
445
+
446
+ .item-card:hover {
447
+ background: var(--bg-hover);
448
+ border-color: var(--border-subtle);
449
+ border-left-color: var(--primary);
450
+ transform: translateX(2px);
451
+ }
452
+
453
+ .item-card.active {
454
+ background: linear-gradient(135deg, var(--primary-dim), var(--accent-dim));
455
+ border-color: rgba(122, 162, 247, 0.3);
456
+ border-left-color: var(--primary);
457
+ box-shadow: 0 0 16px var(--primary-glow);
458
+ }
459
+
460
+ .item-card .item-name {
461
+ font-size: 13px;
462
+ font-weight: 500;
463
+ margin-bottom: 2px;
464
+ display: flex;
465
+ align-items: center;
466
+ gap: 6px;
467
+ color: var(--text-primary);
468
+ }
469
+
470
+ .item-card.active .item-name { color: var(--primary); }
471
+
472
+ .item-card .item-desc {
473
+ font-size: 11px;
474
+ color: var(--text-muted);
475
+ line-height: 1.4;
476
+ display: -webkit-box;
477
+ -webkit-line-clamp: 2;
478
+ -webkit-box-orient: vertical;
479
+ overflow: hidden;
480
+ }
481
+
482
+ .source-badge {
483
+ font-family: 'JetBrains Mono', monospace;
484
+ font-size: 8px;
485
+ font-weight: 500;
486
+ padding: 2px 5px;
487
+ border-radius: 3px;
488
+ text-transform: uppercase;
489
+ letter-spacing: 0.3px;
490
+ }
491
+
492
+ .source-badge.global {
493
+ background: var(--primary-dim);
494
+ color: var(--primary);
495
+ border: 1px solid rgba(122, 162, 247, 0.12);
496
+ }
497
+ .source-badge.project {
498
+ background: var(--success-dim);
499
+ color: var(--success);
500
+ border: 1px solid rgba(115, 218, 202, 0.12);
501
+ }
502
+ .source-badge.claude {
503
+ background: var(--accent-dim);
504
+ color: var(--accent);
505
+ border: 1px solid rgba(187, 154, 247, 0.15);
506
+ }
507
+
508
+ .empty-state {
509
+ padding: 40px 20px;
510
+ text-align: center;
511
+ color: var(--text-muted);
512
+ font-size: 13px;
513
+ line-height: 1.6;
514
+ }
515
+
516
+ .empty-state .empty-icon {
517
+ font-size: 28px;
518
+ margin-bottom: 10px;
519
+ opacity: 0.3;
520
+ display: block;
521
+ }
522
+
523
+ /* ── Main Pane ───────────────────────────────────────── */
524
+
525
+ .main-pane {
526
+ flex: 1;
527
+ display: flex;
528
+ flex-direction: column;
529
+ min-width: 0;
530
+ background: rgba(19, 20, 29, 0.3);
531
+ }
532
+
533
+ .pane-header {
534
+ padding: 18px 28px;
535
+ display: flex;
536
+ align-items: center;
537
+ justify-content: space-between;
538
+ flex-shrink: 0;
539
+ border-bottom: 1px solid var(--border-subtle);
540
+ background: rgba(26, 27, 38, 0.4);
541
+ backdrop-filter: blur(4px);
542
+ }
543
+
544
+ .pane-header h2 {
545
+ font-family: 'JetBrains Mono', monospace;
546
+ font-size: 16px;
547
+ font-weight: 600;
548
+ letter-spacing: -0.2px;
549
+ }
550
+
551
+ .pane-header h2 .pane-source {
552
+ font-family: 'JetBrains Mono', monospace;
553
+ font-size: 10px;
554
+ font-weight: 400;
555
+ color: var(--text-muted);
556
+ margin-left: 8px;
557
+ letter-spacing: 0;
558
+ }
559
+
560
+ .pane-actions { display: flex; gap: 6px; }
561
+
562
+ .btn {
563
+ padding: 7px 14px;
564
+ border: none;
565
+ border-radius: var(--radius-xs);
566
+ font-family: 'JetBrains Mono', monospace;
567
+ font-size: 11px;
568
+ font-weight: 600;
569
+ cursor: pointer;
570
+ transition: all 0.2s ease;
571
+ letter-spacing: 0.3px;
572
+ text-transform: uppercase;
573
+ position: relative;
574
+ overflow: hidden;
575
+ }
576
+
577
+ .btn::after {
578
+ content: '';
579
+ position: absolute;
580
+ inset: 0;
581
+ background: linear-gradient(135deg, transparent 40%, rgba(255,255,255,0.06) 100%);
582
+ pointer-events: none;
583
+ }
584
+
585
+ .btn-primary {
586
+ background: linear-gradient(135deg, var(--accent), var(--primary));
587
+ color: #13141d;
588
+ font-weight: 700;
589
+ }
590
+ .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 16px var(--primary-glow); }
591
+
592
+ .btn-secondary {
593
+ background: var(--bg-elevated);
594
+ color: var(--text-secondary);
595
+ border: 1px solid var(--border-mid);
596
+ }
597
+ .btn-secondary:hover { background: var(--bg-hover); border-color: var(--text-muted); color: var(--text-primary); }
598
+
599
+ .btn-danger {
600
+ background: var(--error-dim);
601
+ color: var(--error);
602
+ border: 1px solid rgba(247, 118, 142, 0.2);
603
+ }
604
+ .btn-danger:hover { background: rgba(247, 118, 142, 0.25); }
605
+
606
+ .btn-success {
607
+ background: linear-gradient(135deg, #0d9488, var(--success));
608
+ color: #13141d;
609
+ }
610
+ .btn-success:hover { transform: translateY(-1px); box-shadow: 0 4px 16px var(--success-glow); }
611
+
612
+ .btn-sm {
613
+ padding: 5px 10px;
614
+ font-size: 10px;
615
+ }
616
+
617
+ .pane-content {
618
+ flex: 1;
619
+ overflow-y: auto;
620
+ padding: 28px;
621
+ }
622
+
623
+ .pane-content::-webkit-scrollbar { width: 4px; }
624
+ .pane-content::-webkit-scrollbar-track { background: transparent; }
625
+ .pane-content::-webkit-scrollbar-thumb { background: var(--border-mid); border-radius: 4px; }
626
+
627
+ /* ── Preview ─────────────────────────────────────────── */
628
+
629
+ .preview {
630
+ font-size: 14px;
631
+ line-height: 1.75;
632
+ color: var(--text-primary);
633
+ max-width: 800px;
634
+ animation: fade-in 0.25s ease;
635
+ }
636
+
637
+ @keyframes fade-in {
638
+ from { opacity: 0; transform: translateY(4px); }
639
+ to { opacity: 1; transform: translateY(0); }
640
+ }
641
+
642
+ .preview h1 {
643
+ font-family: 'JetBrains Mono', monospace;
644
+ font-size: 26px;
645
+ font-weight: 700;
646
+ margin: 0 0 16px;
647
+ color: var(--primary);
648
+ letter-spacing: -0.5px;
649
+ line-height: 1.2;
650
+ }
651
+
652
+ .preview h2 {
653
+ font-family: 'JetBrains Mono', monospace;
654
+ font-size: 18px;
655
+ font-weight: 600;
656
+ margin: 28px 0 12px;
657
+ color: var(--text-primary);
658
+ letter-spacing: -0.3px;
659
+ }
660
+
661
+ .preview h3 {
662
+ font-size: 15px;
663
+ font-weight: 600;
664
+ margin: 22px 0 8px;
665
+ color: var(--text-secondary);
666
+ }
667
+
668
+ .preview p { margin: 0 0 14px; }
669
+ .preview a { color: var(--success); text-decoration: none; border-bottom: 1px solid rgba(115, 218, 202, 0.3); transition: border-color 0.2s; }
670
+ .preview a:hover { border-bottom-color: var(--success); }
671
+
672
+ .preview code {
673
+ font-family: 'JetBrains Mono', monospace;
674
+ font-size: 13px;
675
+ background: rgba(36, 40, 59, 0.8);
676
+ padding: 2px 7px;
677
+ border-radius: 4px;
678
+ border: 1px solid var(--border-subtle);
679
+ color: var(--primary);
680
+ }
681
+
682
+ .preview pre {
683
+ background: rgba(19, 20, 29, 0.8);
684
+ border: 1px solid var(--border-subtle);
685
+ border-radius: var(--radius-sm);
686
+ padding: 18px;
687
+ overflow-x: auto;
688
+ margin: 16px 0;
689
+ position: relative;
690
+ }
691
+
692
+ .preview pre::before {
693
+ content: '```';
694
+ position: absolute;
695
+ top: 6px; right: 12px;
696
+ font-family: 'JetBrains Mono', monospace;
697
+ font-size: 10px;
698
+ color: var(--text-muted);
699
+ opacity: 0.4;
700
+ }
701
+
702
+ .preview pre code { background: none; border: none; padding: 0; color: var(--text-primary); }
703
+
704
+ .preview ul, .preview ol { margin: 0 0 14px; padding-left: 24px; }
705
+ .preview li { margin-bottom: 4px; }
706
+ .preview li::marker { color: var(--primary); }
707
+
708
+ .preview strong { color: var(--text-primary); font-weight: 600; }
709
+ .preview hr { border: none; border-top: 1px solid var(--border-subtle); margin: 28px 0; }
710
+ .preview blockquote {
711
+ border-left: 3px solid var(--accent);
712
+ padding: 10px 18px;
713
+ margin: 16px 0;
714
+ background: var(--accent-dim);
715
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
716
+ color: var(--text-secondary);
717
+ font-style: italic;
718
+ }
719
+
720
+ .preview table {
721
+ width: 100%;
722
+ border-collapse: collapse;
723
+ margin: 16px 0;
724
+ font-size: 13px;
725
+ border-radius: var(--radius-xs);
726
+ overflow: hidden;
727
+ }
728
+
729
+ .preview th, .preview td {
730
+ padding: 10px 14px;
731
+ border: 1px solid var(--border-subtle);
732
+ text-align: left;
733
+ }
734
+
735
+ .preview th {
736
+ background: rgba(36, 40, 59, 0.8);
737
+ font-weight: 600;
738
+ font-family: 'JetBrains Mono', monospace;
739
+ font-size: 11px;
740
+ text-transform: uppercase;
741
+ letter-spacing: 0.3px;
742
+ color: var(--text-secondary);
743
+ }
744
+
745
+ .preview tr:hover td { background: rgba(36, 40, 59, 0.4); }
746
+
747
+ /* ── Editor ──────────────────────────────────────────── */
748
+
749
+ .editor textarea {
750
+ width: 100%;
751
+ min-height: 400px;
752
+ background: rgba(19, 20, 29, 0.8);
753
+ border: 1px solid var(--border-mid);
754
+ border-radius: var(--radius-sm);
755
+ color: var(--text-primary);
756
+ font-family: 'JetBrains Mono', monospace;
757
+ font-size: 13px;
758
+ line-height: 1.7;
759
+ padding: 18px;
760
+ resize: vertical;
761
+ outline: none;
762
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
763
+ }
764
+
765
+ .editor textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-glow); }
766
+
767
+ .editor .editor-actions {
768
+ display: flex;
769
+ gap: 8px;
770
+ margin-top: 14px;
771
+ }
772
+
773
+ /* ── Create Form ──────────────────────────────────────── */
774
+
775
+ .create-form { max-width: 640px; animation: fade-in 0.25s ease; }
776
+
777
+ .form-group { margin-bottom: 18px; }
778
+
779
+ .form-group label {
780
+ display: block;
781
+ font-family: 'JetBrains Mono', monospace;
782
+ font-size: 11px;
783
+ font-weight: 600;
784
+ color: var(--text-muted);
785
+ margin-bottom: 6px;
786
+ text-transform: uppercase;
787
+ letter-spacing: 0.8px;
788
+ }
789
+
790
+ .form-group input, .form-group textarea, .form-group select {
791
+ width: 100%;
792
+ padding: 11px 14px;
793
+ background: rgba(19, 20, 29, 0.8);
794
+ border: 1px solid var(--border-mid);
795
+ border-radius: var(--radius-xs);
796
+ color: var(--text-primary);
797
+ font-family: 'JetBrains Mono', monospace;
798
+ font-size: 14px;
799
+ outline: none;
800
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
801
+ }
802
+
803
+ .form-group input:focus, .form-group textarea:focus, .form-group select:focus {
804
+ border-color: var(--primary);
805
+ box-shadow: 0 0 0 3px var(--primary-glow);
806
+ }
807
+
808
+ .form-group textarea {
809
+ min-height: 240px;
810
+ font-family: 'JetBrains Mono', monospace;
811
+ font-size: 13px;
812
+ resize: vertical;
813
+ }
814
+
815
+ .form-group select {
816
+ cursor: pointer;
817
+ appearance: none;
818
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23565f89' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
819
+ background-repeat: no-repeat;
820
+ background-position: right 14px center;
821
+ padding-right: 36px;
822
+ }
823
+
824
+ .form-actions { display: flex; gap: 8px; margin-top: 24px; }
825
+
826
+ /* ── Toast / Notifications ───────────────────────────── */
827
+
828
+ .toast {
829
+ position: fixed;
830
+ bottom: 28px;
831
+ right: 28px;
832
+ z-index: 100;
833
+ padding: 14px 22px;
834
+ border-radius: var(--radius-xs);
835
+ font-size: 13px;
836
+ font-weight: 500;
837
+ animation: toast-in 0.35s ease;
838
+ max-width: 420px;
839
+ backdrop-filter: blur(12px);
840
+ font-family: 'JetBrains Mono', monospace;
841
+ }
842
+
843
+ .toast.success {
844
+ background: var(--success-dim);
845
+ color: var(--success);
846
+ border: 1px solid rgba(115, 218, 202, 0.2);
847
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
848
+ }
849
+ .toast.error {
850
+ background: var(--error-dim);
851
+ color: var(--error);
852
+ border: 1px solid rgba(247, 118, 142, 0.2);
853
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
854
+ }
855
+
856
+ @keyframes toast-in {
857
+ from { opacity: 0; transform: translateY(12px) scale(0.95); }
858
+ to { opacity: 1; transform: translateY(0) scale(1); }
859
+ }
860
+
861
+ /* ── Loading ──────────────────────────────────────────── */
862
+
863
+ .loading {
864
+ display: flex;
865
+ align-items: center;
866
+ justify-content: center;
867
+ padding: 60px;
868
+ color: var(--text-muted);
869
+ font-size: 13px;
870
+ font-weight: 400;
871
+ }
872
+
873
+ .loading::after {
874
+ content: '';
875
+ width: 14px; height: 14px;
876
+ margin-left: 10px;
877
+ border: 2px solid var(--border-mid);
878
+ border-top-color: var(--primary);
879
+ border-radius: 50%;
880
+ animation: spin 0.7s linear infinite;
881
+ }
882
+
883
+ @keyframes spin { to { transform: rotate(360deg); } }
884
+
885
+ /* ── Responsive ──────────────────────────────────────── */
886
+
887
+ @media (max-width: 768px) {
888
+ .sidebar { width: 100%; min-width: 0; flex: 0 0 min(36vh, 280px); border-right: none; border-bottom: 1px solid var(--border-subtle); }
889
+ body { overflow-y: auto; }
890
+ .app-container { flex: 1 0 auto; min-height: calc(100dvh - 80px); flex-direction: column; }
891
+ .main-pane { flex: 1 0 auto; min-height: 58vh; }
892
+ header { flex-direction: column; align-items: flex-start; gap: 10px; }
893
+ .pane-header { flex-direction: column; align-items: flex-start; gap: 10px; }
894
+ .pane-actions { flex-wrap: wrap; }
895
+ .pane-content { padding: 18px 14px; }
896
+ }
897
+
898
+ @media (max-width: 480px) {
899
+ header { padding: 14px 16px; }
900
+ .sidebar { flex-basis: 32vh; }
901
+ .tab-bar { padding: 8px 8px 0; }
902
+ .tab-btn { min-height: 42px; }
903
+ .pane-header { padding: 14px 16px; }
904
+ .pane-header h2 { overflow-wrap: anywhere; }
905
+ .pane-actions .btn { min-height: 40px; }
906
+ }
907
+ </style>
908
+ </head>
909
+ <body>
910
+ <div class="grain"></div>
911
+ <div class="particle-field">
912
+ <div class="particle"></div>
913
+ <div class="particle"></div>
914
+ <div class="particle"></div>
915
+ <div class="particle"></div>
916
+ <div class="particle"></div>
917
+ <div class="particle"></div>
918
+ </div>
919
+
920
+ <header>
921
+ <div class="header-left">
922
+ <div class="logo-icon">N</div>
923
+ <div>
924
+ <h1>Nova</h1>
925
+ <span class="subtitle">Skills &amp; Agents &mdash; tokyo-night</span>
926
+ </div>
927
+ </div>
928
+ <div class="header-right">
929
+ <div class="server-badge">
930
+ <span class="live-dot"></span>
931
+ LIVE
932
+ </div>
933
+ </div>
934
+ </header>
935
+
936
+ <div class="app-container">
937
+ <!-- Sidebar -->
938
+ <div class="sidebar">
939
+ <div class="tab-bar">
940
+ <button class="tab-btn active" data-tab="skills" onclick="switchTab('skills')">
941
+ Skills <span class="count" id="skills-count">0</span>
942
+ </button>
943
+ <button class="tab-btn" data-tab="agents" onclick="switchTab('agents')">
944
+ Agents <span class="count" id="agents-count">0</span>
945
+ </button>
946
+ </div>
947
+ <div class="sidebar-search">
948
+ <input type="text" id="search-input" placeholder="Filter items..." oninput="filterList(this.value)">
949
+ </div>
950
+ <div class="item-list" id="item-list">
951
+ <div class="empty-state">
952
+ <span class="empty-icon">◈</span>
953
+ <div>No items loaded</div>
954
+ </div>
955
+ </div>
956
+ </div>
957
+
958
+ <!-- Main Pane -->
959
+ <div class="main-pane">
960
+ <div class="pane-header">
961
+ <h2 id="pane-title">Select an item</h2>
962
+ <div class="pane-actions" id="pane-actions">
963
+ <button class="btn btn-primary" onclick="showCreateForm()">+ New</button>
964
+ </div>
965
+ </div>
966
+ <div class="pane-content" id="pane-content">
967
+ <div class="empty-state">
968
+ <span class="empty-icon">◈</span>
969
+ <div>Choose a skill or agent from the sidebar to inspect its contents, or create something new.</div>
970
+ </div>
971
+ </div>
972
+ </div>
973
+ </div>
974
+
975
+ <div id="toast-container"></div>
976
+
977
+ <script>
978
+ // ── State ──────────────────────────────────────────────────
979
+
980
+ let currentTab = 'skills';
981
+ let skills = [];
982
+ let agents = [];
983
+ let selectedItem = null;
984
+ let isEditing = false;
985
+ let searchQuery = '';
986
+
987
+ // ── API Helpers ─────────────────────────────────────────────
988
+
989
+ async function api(method, path, body) {
990
+ const opts = { method, headers: {'X-Nova-Request': '1'} };
991
+ if (body) {
992
+ opts.headers['Content-Type'] = 'application/json';
993
+ opts.body = JSON.stringify(body);
994
+ }
995
+ const res = await fetch(path, opts);
996
+ const data = await res.json();
997
+ if (!res.ok) throw new Error(data.error || 'Request failed');
998
+ return data;
999
+ }
1000
+
1001
+ // ── Toast ──────────────────────────────────────────────────
1002
+
1003
+ function showToast(message, type = 'success') {
1004
+ const container = document.getElementById('toast-container');
1005
+ const toast = document.createElement('div');
1006
+ toast.className = `toast ${type}`;
1007
+ toast.textContent = message;
1008
+ container.appendChild(toast);
1009
+ setTimeout(() => { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.3s'; setTimeout(() => toast.remove(), 300); }, 3000);
1010
+ }
1011
+
1012
+ // ── Tab Switching ──────────────────────────────────────────
1013
+
1014
+ function switchTab(tab) {
1015
+ currentTab = tab;
1016
+ selectedItem = null;
1017
+ isEditing = false;
1018
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
1019
+ document.getElementById('search-input').value = '';
1020
+ searchQuery = '';
1021
+ renderList();
1022
+ renderPane();
1023
+ }
1024
+
1025
+ // ── Search Filter ──────────────────────────────────────────
1026
+
1027
+ function filterList(query) {
1028
+ searchQuery = query.toLowerCase().trim();
1029
+ renderList();
1030
+ }
1031
+
1032
+ // ── Data Loading ───────────────────────────────────────────
1033
+
1034
+ async function loadData() {
1035
+ try {
1036
+ skills = await api('GET', '/api/skills');
1037
+ agents = await api('GET', '/api/agents');
1038
+ document.getElementById('skills-count').textContent = skills.length;
1039
+ document.getElementById('agents-count').textContent = agents.length;
1040
+ renderList();
1041
+ if (selectedItem) {
1042
+ const items = currentTab === 'skills' ? skills : agents;
1043
+ const stillExists = items.find(i => i.name === selectedItem.name);
1044
+ if (!stillExists) { selectedItem = null; renderPane(); }
1045
+ }
1046
+ } catch (e) {
1047
+ showToast('Failed to load data: ' + e.message, 'error');
1048
+ }
1049
+ }
1050
+
1051
+ // ── List Rendering ─────────────────────────────────────────
1052
+
1053
+ function renderList() {
1054
+ const el = document.getElementById('item-list');
1055
+ let items = currentTab === 'skills' ? skills : agents;
1056
+
1057
+ // Apply search filter
1058
+ if (searchQuery) {
1059
+ items = items.filter(item =>
1060
+ item.name.toLowerCase().includes(searchQuery) ||
1061
+ (item.description || '').toLowerCase().includes(searchQuery)
1062
+ );
1063
+ }
1064
+
1065
+ if (items.length === 0) {
1066
+ const msg = searchQuery
1067
+ ? `<span class="empty-icon">◈</span><div>No ${currentTab} match "<strong>${escapeHtml(searchQuery)}</strong>"</div>`
1068
+ : `<span class="empty-icon">◈</span><div>No ${currentTab} found. Click <strong>New</strong> to create one.</div>`;
1069
+ el.innerHTML = `<div class="empty-state">${msg}</div>`;
1070
+ return;
1071
+ }
1072
+
1073
+ el.innerHTML = items.map((item, i) => {
1074
+ const isActive = selectedItem && selectedItem.name === item.name;
1075
+ const source = item.source || 'global';
1076
+ return `<div class="item-card ${isActive ? 'active' : ''}" style="animation-delay:${(i % 6) * 0.03}s" onclick="selectItem('${escapeHtml(item.name)}')">
1077
+ <div class="item-name">
1078
+ ${escapeHtml(item.name)}
1079
+ <span class="source-badge ${source}">${source}</span>
1080
+ </div>
1081
+ <div class="item-desc">${escapeHtml(item.description || 'No description')}</div>
1082
+ </div>`;
1083
+ }).join('');
1084
+ }
1085
+
1086
+ // ── Item Selection ─────────────────────────────────────────
1087
+
1088
+ async function selectItem(name) {
1089
+ isEditing = false;
1090
+ try {
1091
+ const data = await api('GET', `/api/${currentTab}/${encodeURIComponent(name)}`);
1092
+ selectedItem = data;
1093
+ renderList();
1094
+ renderPane();
1095
+ } catch (e) {
1096
+ showToast('Failed to load item: ' + e.message, 'error');
1097
+ }
1098
+ }
1099
+
1100
+ // ── Pane Rendering ─────────────────────────────────────────
1101
+
1102
+ function renderPane() {
1103
+ const titleEl = document.getElementById('pane-title');
1104
+ const actionsEl = document.getElementById('pane-actions');
1105
+ const contentEl = document.getElementById('pane-content');
1106
+
1107
+ if (!selectedItem) {
1108
+ titleEl.innerHTML = 'Select an item';
1109
+ actionsEl.innerHTML = '<button class="btn btn-primary" onclick="showCreateForm()">+ New</button>';
1110
+ contentEl.innerHTML = '<div class="empty-state"><span class="empty-icon">◈</span><div>Choose a ' + currentTab.slice(0, -1) + ' from the sidebar to inspect its contents, or create something new.</div></div>';
1111
+ return;
1112
+ }
1113
+
1114
+ const source = selectedItem.source || '';
1115
+ titleEl.innerHTML = `${escapeHtml(selectedItem.name)} <span class="pane-source">${source}</span>`;
1116
+
1117
+ if (isEditing) {
1118
+ actionsEl.innerHTML = `
1119
+ <button class="btn btn-secondary" onclick="cancelEdit()">Cancel</button>
1120
+ <button class="btn btn-success" onclick="saveEdit()">Save</button>
1121
+ `;
1122
+ contentEl.innerHTML = `<div class="editor">
1123
+ <textarea id="editor-textarea">${escapeHtml(selectedItem.content || '')}</textarea>
1124
+ <div class="editor-actions">
1125
+ <button class="btn btn-success btn-sm" onclick="saveEdit()">Save Changes</button>
1126
+ <button class="btn btn-secondary btn-sm" onclick="cancelEdit()">Cancel</button>
1127
+ </div>
1128
+ </div>`;
1129
+ setTimeout(() => {
1130
+ const ta = document.getElementById('editor-textarea');
1131
+ if (ta) ta.focus();
1132
+ }, 50);
1133
+ return;
1134
+ }
1135
+
1136
+ actionsEl.innerHTML = `
1137
+ <button class="btn btn-secondary btn-sm" onclick="startEdit()">Edit</button>
1138
+ <button class="btn btn-danger btn-sm" onclick="deleteItem()">Delete</button>
1139
+ <button class="btn btn-primary btn-sm" onclick="showCreateForm()">+ New</button>
1140
+ `;
1141
+
1142
+ contentEl.innerHTML = `<div class="preview">${renderMarkdown(selectedItem.content || '')}</div>`;
1143
+ }
1144
+
1145
+ // ── Markdown Renderer (simple) ─────────────────────────────
1146
+
1147
+ function renderMarkdown(md) {
1148
+ let html = md
1149
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
1150
+ .replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
1151
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
1152
+ .replace(/^### (.+)$/gm, '<h3>$1</h3>')
1153
+ .replace(/^## (.+)$/gm, '<h2>$1</h2>')
1154
+ .replace(/^# (.+)$/gm, '<h1>$1</h1>')
1155
+ .replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>')
1156
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
1157
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
1158
+ .replace(/^---$/gm, '<hr>')
1159
+ .replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
1160
+ .replace(/^- (.+)$/gm, '<li>$1</li>')
1161
+ .replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
1162
+ .replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
1163
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
1164
+ .replace(/\n\n/g, '</p><p>')
1165
+ .replace(/\n/g, '<br>');
1166
+
1167
+ return '<p>' + html + '</p>';
1168
+ }
1169
+
1170
+ // ── Edit Operations ────────────────────────────────────────
1171
+
1172
+ function startEdit() {
1173
+ isEditing = true;
1174
+ renderPane();
1175
+ }
1176
+
1177
+ function cancelEdit() {
1178
+ isEditing = false;
1179
+ renderPane();
1180
+ }
1181
+
1182
+ async function saveEdit() {
1183
+ const ta = document.getElementById('editor-textarea');
1184
+ if (!ta) return;
1185
+ const content = ta.value;
1186
+ try {
1187
+ await api('PUT', `/api/${currentTab}/${encodeURIComponent(selectedItem.name)}`, { content });
1188
+ showToast(`${currentTab.slice(0, -1)} "${selectedItem.name}" saved`);
1189
+ isEditing = false;
1190
+ const data = await api('GET', `/api/${currentTab}/${encodeURIComponent(selectedItem.name)}`);
1191
+ selectedItem = data;
1192
+ renderPane();
1193
+ loadData();
1194
+ } catch (e) {
1195
+ showToast('Failed to save: ' + e.message, 'error');
1196
+ }
1197
+ }
1198
+
1199
+ async function deleteItem() {
1200
+ if (!confirm(`Delete the ${currentTab.slice(0, -1)} "${selectedItem.name}"?`)) return;
1201
+ try {
1202
+ await api('DELETE', `/api/${currentTab}/${encodeURIComponent(selectedItem.name)}`);
1203
+ showToast(`${currentTab.slice(0, -1)} "${selectedItem.name}" deleted`);
1204
+ selectedItem = null;
1205
+ loadData();
1206
+ } catch (e) {
1207
+ showToast('Failed to delete: ' + e.message, 'error');
1208
+ }
1209
+ }
1210
+
1211
+ // ── Create Form ────────────────────────────────────────────
1212
+
1213
+ async function showCreateForm() {
1214
+ isEditing = false;
1215
+ selectedItem = null;
1216
+ renderList();
1217
+
1218
+ const titleEl = document.getElementById('pane-title');
1219
+ const actionsEl = document.getElementById('pane-actions');
1220
+ const contentEl = document.getElementById('pane-content');
1221
+
1222
+ titleEl.textContent = 'Create New ' + (currentTab === 'skills' ? 'Skill' : 'Agent');
1223
+
1224
+ actionsEl.innerHTML = `
1225
+ <button class="btn btn-secondary btn-sm" onclick="cancelCreate()">Cancel</button>
1226
+ <button class="btn btn-success btn-sm" onclick="submitCreate()">Create</button>
1227
+ `;
1228
+
1229
+ contentEl.innerHTML = `<div class="create-form">
1230
+ <div class="form-group">
1231
+ <label for="create-name">Name</label>
1232
+ <input type="text" id="create-name" placeholder="e.g., my-custom-skill" autofocus>
1233
+ </div>
1234
+ <div class="form-group">
1235
+ <label for="create-desc">Description</label>
1236
+ <input type="text" id="create-desc" placeholder="Brief description of what this does">
1237
+ </div>
1238
+ <div class="form-group">
1239
+ <label for="create-scope">Scope</label>
1240
+ <select id="create-scope">
1241
+ <option value="global">Global (~/.nova/${currentTab}/)</option>
1242
+ <option value="project">Project (.nova/${currentTab}/)</option>
1243
+ </select>
1244
+ </div>
1245
+ <div class="form-group">
1246
+ <label for="create-content">Content (Markdown)</label>
1247
+ <textarea id="create-content" placeholder="Write the ${currentTab === 'skills' ? 'SKILL.md' : 'agent.md'} content here...">---
1248
+ name:
1249
+ description:
1250
+ ---
1251
+
1252
+ # ${currentTab === 'skills' ? 'My Skill' : 'My Agent'}
1253
+
1254
+ Write your content here...</textarea>
1255
+ </div>
1256
+ <div class="form-actions">
1257
+ <button class="btn btn-success" onclick="submitCreate()">Create</button>
1258
+ <button class="btn btn-secondary" onclick="cancelCreate()">Cancel</button>
1259
+ </div>
1260
+ </div>`;
1261
+
1262
+ setTimeout(() => {
1263
+ const el = document.getElementById('create-name');
1264
+ if (el) el.focus();
1265
+ }, 50);
1266
+ }
1267
+
1268
+ function cancelCreate() {
1269
+ selectedItem = null;
1270
+ renderList();
1271
+ renderPane();
1272
+ }
1273
+
1274
+ async function submitCreate() {
1275
+ const name = document.getElementById('create-name').value.trim();
1276
+ const description = document.getElementById('create-desc').value.trim();
1277
+ const scope = document.getElementById('create-scope').value;
1278
+ const content = document.getElementById('create-content').value;
1279
+
1280
+ if (!name) { showToast('Name is required', 'error'); return; }
1281
+
1282
+ try {
1283
+ await api('POST', `/api/${currentTab}`, { name, description, content, scope });
1284
+ showToast(`${currentTab.slice(0, -1)} "${name}" created`);
1285
+ await loadData();
1286
+ await selectItem(name);
1287
+ } catch (e) {
1288
+ showToast('Failed to create: ' + e.message, 'error');
1289
+ }
1290
+ }
1291
+
1292
+ // ── Utility ────────────────────────────────────────────────
1293
+
1294
+ function escapeHtml(text) {
1295
+ if (!text) return '';
1296
+ const div = document.createElement('div');
1297
+ div.textContent = text;
1298
+ return div.innerHTML;
1299
+ }
1300
+
1301
+ // ── Init ───────────────────────────────────────────────────
1302
+
1303
+ loadData();
1304
+ </script>
1305
+ </body>
1306
+ </html>"""
1307
+
1308
+
1309
+ # ---------------------------------------------------------------------------
1310
+ # Request Handler
1311
+ # ---------------------------------------------------------------------------
1312
+
1313
+
1314
+ from novacode_cli.security.local_http import LocalRequestHandler
1315
+
1316
+
1317
+ class CreateRequestHandler(LocalRequestHandler):
1318
+ """HTTP request handler for the Create web UI API and static page."""
1319
+
1320
+ # Shared state — set by CreateServer
1321
+ settings: Settings | None = None
1322
+ lock: threading.Lock = threading.Lock()
1323
+
1324
+ def log_message(self, format: str, *args: Any) -> None:
1325
+ """Suppress default HTTP server logging."""
1326
+ pass
1327
+
1328
+ def _send_json(self, data: Any, status: int = 200) -> None:
1329
+ self.send_response(status)
1330
+ self.send_header("Content-Type", "application/json")
1331
+ self.send_header("Access-Control-Allow-Origin", "*")
1332
+ self.end_headers()
1333
+ self.wfile.write(json.dumps(data).encode("utf-8"))
1334
+
1335
+ def _send_html(self, html: str, status: int = 200) -> None:
1336
+ from novacode_cli.security.local_http import secure_html
1337
+ html = secure_html(html)
1338
+ self.send_response(status)
1339
+ self.send_header("Content-Type", "text/html; charset=utf-8")
1340
+ self.end_headers()
1341
+ self.wfile.write(html.encode("utf-8"))
1342
+
1343
+ def _send_error(self, message: str, status: int = 400) -> None:
1344
+ self._send_json({"error": message}, status)
1345
+
1346
+ def _read_body(self) -> dict:
1347
+ content_length = int(self.headers.get("Content-Length", 0))
1348
+ if content_length == 0:
1349
+ return {}
1350
+ body = self.rfile.read(content_length)
1351
+ return json.loads(body.decode("utf-8"))
1352
+
1353
+ def _get_path_parts(self) -> list[str]:
1354
+ """Parse path into parts, handling /api/<type>/<name>."""
1355
+ path = self.path.rstrip("/")
1356
+ parts = path.split("/")
1357
+ parts = [p for p in parts if p]
1358
+ if len(parts) == 3 and parts[0] == "api" and not _is_valid_name(parts[2]):
1359
+ return []
1360
+ return parts
1361
+
1362
+ # ── CORS ────────────────────────────────────────────────────────
1363
+
1364
+ def do_OPTIONS(self) -> None:
1365
+ self.send_response(204)
1366
+ self.send_header("Access-Control-Allow-Origin", "*")
1367
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
1368
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
1369
+ self.end_headers()
1370
+
1371
+ # ── GET ────────────────────────────────────────────────────────
1372
+
1373
+ def do_GET(self) -> None:
1374
+ parts = self._get_path_parts()
1375
+
1376
+ # Serve the HTML page
1377
+ if self.path == "/" or self.path == "":
1378
+ self._send_html(HTML_PAGE)
1379
+ return
1380
+
1381
+ # GET /api/skills — list all skills
1382
+ if parts == ["api", "skills"]:
1383
+ self._send_json(self._list_skills())
1384
+ return
1385
+
1386
+ # GET /api/skills/<name> — get skill content
1387
+ if len(parts) == 3 and parts[0] == "api" and parts[1] == "skills":
1388
+ result = self._get_skill(parts[2])
1389
+ if result is not None:
1390
+ self._send_json(result)
1391
+ else:
1392
+ self._send_error(f"Skill '{parts[2]}' not found", 404)
1393
+ return
1394
+
1395
+ # GET /api/agents — list all agents
1396
+ if parts == ["api", "agents"]:
1397
+ self._send_json(self._list_agents())
1398
+ return
1399
+
1400
+ # GET /api/agents/<name> — get agent content
1401
+ if len(parts) == 3 and parts[0] == "api" and parts[1] == "agents":
1402
+ result = self._get_agent(parts[2])
1403
+ if result is not None:
1404
+ self._send_json(result)
1405
+ else:
1406
+ self._send_error(f"Agent '{parts[2]}' not found", 404)
1407
+ return
1408
+
1409
+ self._send_error("Not found", 404)
1410
+
1411
+ # ── POST ───────────────────────────────────────────────────────
1412
+
1413
+ def do_POST(self) -> None:
1414
+ parts = self._get_path_parts()
1415
+ body = self._read_body()
1416
+
1417
+ # POST /api/skills — create a new skill
1418
+ if parts == ["api", "skills"]:
1419
+ self._create_skill(body)
1420
+ return
1421
+
1422
+ # POST /api/agents — create a new agent
1423
+ if parts == ["api", "agents"]:
1424
+ self._create_agent(body)
1425
+ return
1426
+
1427
+ self._send_error("Not found", 404)
1428
+
1429
+ # ── PUT ────────────────────────────────────────────────────────
1430
+
1431
+ def do_PUT(self) -> None:
1432
+ parts = self._get_path_parts()
1433
+ body = self._read_body()
1434
+
1435
+ # PUT /api/skills/<name> — update skill content
1436
+ if len(parts) == 3 and parts[0] == "api" and parts[1] == "skills":
1437
+ self._update_skill(parts[2], body)
1438
+ return
1439
+
1440
+ # PUT /api/agents/<name> — update agent content
1441
+ if len(parts) == 3 and parts[0] == "api" and parts[1] == "agents":
1442
+ self._update_agent(parts[2], body)
1443
+ return
1444
+
1445
+ self._send_error("Not found", 404)
1446
+
1447
+ # ── DELETE ─────────────────────────────────────────────────────
1448
+
1449
+ def do_DELETE(self) -> None:
1450
+ parts = self._get_path_parts()
1451
+
1452
+ # DELETE /api/skills/<name> — delete a skill
1453
+ if len(parts) == 3 and parts[0] == "api" and parts[1] == "skills":
1454
+ self._delete_skill(parts[2])
1455
+ return
1456
+
1457
+ # DELETE /api/agents/<name> — delete an agent
1458
+ if len(parts) == 3 and parts[0] == "api" and parts[1] == "agents":
1459
+ self._delete_agent(parts[2])
1460
+ return
1461
+
1462
+ self._send_error("Not found", 404)
1463
+
1464
+ # ── Skills CRUD ────────────────────────────────────────────────
1465
+
1466
+ def _list_skills(self) -> list[dict]:
1467
+ """List all skills from global and project scopes."""
1468
+ settings = self.settings
1469
+ if not settings:
1470
+ return []
1471
+
1472
+ result: list[dict] = []
1473
+ seen: set[str] = set()
1474
+
1475
+ # Global skills (~/.nova/skills/)
1476
+ global_dir = settings.get_global_skills_dir()
1477
+ if global_dir.exists():
1478
+ for skill_dir in sorted(global_dir.iterdir()):
1479
+ if skill_dir.is_dir():
1480
+ skill_md = skill_dir / "SKILL.md"
1481
+ if skill_md.exists():
1482
+ content = skill_md.read_text(encoding="utf-8", errors="replace")
1483
+ name = _extract_frontmatter_field(content, "name") or skill_dir.name
1484
+ description = _extract_frontmatter_field(content, "description") or ""
1485
+ result.append({
1486
+ "name": name,
1487
+ "description": description[:120],
1488
+ "source": "global",
1489
+ "path": str(skill_dir),
1490
+ })
1491
+ seen.add(name)
1492
+
1493
+ # Project skills (.nova/skills/)
1494
+ project_dir = settings.get_project_skills_dir()
1495
+ if project_dir and project_dir.exists():
1496
+ for skill_dir in sorted(project_dir.iterdir()):
1497
+ if skill_dir.is_dir():
1498
+ skill_md = skill_dir / "SKILL.md"
1499
+ if skill_md.exists():
1500
+ content = skill_md.read_text(encoding="utf-8", errors="replace")
1501
+ name = _extract_frontmatter_field(content, "name") or skill_dir.name
1502
+ description = _extract_frontmatter_field(content, "description") or ""
1503
+ # Project skills override global ones with same name
1504
+ if name in seen:
1505
+ result = [r for r in result if r["name"] != name]
1506
+ result.append({
1507
+ "name": name,
1508
+ "description": description[:120],
1509
+ "source": "project",
1510
+ "path": str(skill_dir),
1511
+ })
1512
+ seen.add(name)
1513
+
1514
+ return result
1515
+
1516
+ def _get_skill(self, name: str) -> dict | None:
1517
+ """Get a skill's full SKILL.md content by name.
1518
+
1519
+ Returns the skill dict, or None if not found (caller sends 404).
1520
+ """
1521
+ settings = self.settings
1522
+ if not settings:
1523
+ return None
1524
+
1525
+ # Check project first, then global
1526
+ project_dir = settings.get_project_skills_dir()
1527
+ if project_dir:
1528
+ skill_dir = project_dir / name
1529
+ skill_md = skill_dir / "SKILL.md"
1530
+ if skill_md.exists():
1531
+ content = skill_md.read_text(encoding="utf-8", errors="replace")
1532
+ return {"name": name, "content": content, "source": "project", "path": str(skill_dir)}
1533
+
1534
+ global_dir = settings.get_global_skills_dir()
1535
+ skill_dir = global_dir / name
1536
+ skill_md = skill_dir / "SKILL.md"
1537
+ if skill_md.exists():
1538
+ content = skill_md.read_text(encoding="utf-8", errors="replace")
1539
+ return {"name": name, "content": content, "source": "global", "path": str(skill_dir)}
1540
+
1541
+ return None
1542
+
1543
+ def _create_skill(self, body: dict) -> None: # noqa: PLR0911 - Validate before creating files.
1544
+ """Create a new skill."""
1545
+ name = body.get("name", "").strip()
1546
+ description = body.get("description", "").strip()
1547
+ content = body.get("content", "")
1548
+ scope = body.get("scope", "global")
1549
+
1550
+ if not name:
1551
+ self._send_error("Name is required")
1552
+ return
1553
+ if not _is_valid_name(name):
1554
+ self._send_error("Invalid name. Use only letters, numbers, hyphens, and underscores.")
1555
+ return
1556
+
1557
+ settings = self.settings
1558
+ if not settings:
1559
+ self._send_error("Settings not available", 500)
1560
+ return
1561
+
1562
+ # Determine target directory
1563
+ if scope == "project":
1564
+ base_dir = settings.get_project_skills_dir()
1565
+ if not base_dir:
1566
+ self._send_error("Not in a project directory", 400)
1567
+ return
1568
+ else:
1569
+ base_dir = settings.get_global_skills_dir()
1570
+
1571
+ skill_dir = base_dir / name
1572
+ if skill_dir.exists():
1573
+ self._send_error(f"Skill '{name}' already exists", 409)
1574
+ return
1575
+
1576
+ from novacode_cli.skills.schema import prepare_skill_content
1577
+
1578
+ try:
1579
+ final_content = prepare_skill_content(content, name, description)
1580
+ except ValueError as exc:
1581
+ self._send_error(str(exc), 400)
1582
+ return
1583
+
1584
+ # Write atomically
1585
+ skill_dir.mkdir(parents=True, exist_ok=True)
1586
+ skill_md = skill_dir / "SKILL.md"
1587
+ tmp_path = skill_dir / f".tmp.{os.getpid()}"
1588
+ try:
1589
+ tmp_path.write_text(final_content, encoding="utf-8")
1590
+ tmp_path.replace(skill_md)
1591
+ except Exception as e:
1592
+ # Clean up on failure
1593
+ if tmp_path.exists():
1594
+ tmp_path.unlink(missing_ok=True)
1595
+ skill_dir.rmdir()
1596
+ self._send_error(f"Failed to create skill: {e}", 500)
1597
+ return
1598
+
1599
+ self._send_json({"name": name, "source": scope, "path": str(skill_dir)}, 201)
1600
+
1601
+ def _update_skill(self, name: str, body: dict) -> None:
1602
+ """Update a skill's SKILL.md content."""
1603
+ content = body.get("content", "")
1604
+ if not content:
1605
+ self._send_error("Content is required")
1606
+ return
1607
+
1608
+ settings = self.settings
1609
+ if not settings:
1610
+ self._send_error("Settings not available", 500)
1611
+ return
1612
+
1613
+ # Find the skill directory
1614
+ project_dir = settings.get_project_skills_dir()
1615
+ skill_dir = None
1616
+ if project_dir:
1617
+ candidate = project_dir / name
1618
+ if (candidate / "SKILL.md").exists():
1619
+ skill_dir = candidate
1620
+
1621
+ if not skill_dir:
1622
+ global_dir = settings.get_global_skills_dir()
1623
+ candidate = global_dir / name
1624
+ if (candidate / "SKILL.md").exists():
1625
+ skill_dir = candidate
1626
+
1627
+ if not skill_dir:
1628
+ self._send_error(f"Skill '{name}' not found", 404)
1629
+ return
1630
+
1631
+ from novacode_cli.skills.schema import prepare_skill_content
1632
+
1633
+ try:
1634
+ content = prepare_skill_content(content, name)
1635
+ except ValueError as exc:
1636
+ self._send_error(str(exc), 400)
1637
+ return
1638
+
1639
+ # Write atomically
1640
+ skill_md = skill_dir / "SKILL.md"
1641
+ tmp_path = skill_dir / f".tmp.{os.getpid()}"
1642
+ try:
1643
+ tmp_path.write_text(content, encoding="utf-8")
1644
+ tmp_path.replace(skill_md)
1645
+ except Exception as e:
1646
+ if tmp_path.exists():
1647
+ tmp_path.unlink(missing_ok=True)
1648
+ self._send_error(f"Failed to save: {e}", 500)
1649
+ return
1650
+
1651
+ self._send_json({"name": name, "status": "saved"})
1652
+
1653
+ def _delete_skill(self, name: str) -> None:
1654
+ """Delete a skill directory."""
1655
+ settings = self.settings
1656
+ if not settings:
1657
+ self._send_error("Settings not available", 500)
1658
+ return
1659
+
1660
+ import shutil
1661
+
1662
+ # Find the skill directory
1663
+ project_dir = settings.get_project_skills_dir()
1664
+ skill_dir = None
1665
+ if project_dir:
1666
+ candidate = project_dir / name
1667
+ if candidate.exists():
1668
+ skill_dir = candidate
1669
+
1670
+ if not skill_dir:
1671
+ global_dir = settings.get_global_skills_dir()
1672
+ candidate = global_dir / name
1673
+ if candidate.exists():
1674
+ skill_dir = candidate
1675
+
1676
+ if not skill_dir:
1677
+ self._send_error(f"Skill '{name}' not found", 404)
1678
+ return
1679
+
1680
+ try:
1681
+ shutil.rmtree(skill_dir)
1682
+ except Exception as e:
1683
+ self._send_error(f"Failed to delete: {e}", 500)
1684
+ return
1685
+
1686
+ self._send_json({"name": name, "status": "deleted"})
1687
+
1688
+ # ── Agents CRUD ─────────────────────────────────────────────────
1689
+
1690
+ def _list_agents(self) -> list[dict]:
1691
+ """List all agents from global and project scopes (folder or flat file)."""
1692
+ settings = self.settings
1693
+ if not settings:
1694
+ return []
1695
+
1696
+ # get_all_agents lists project agents first, so keeping the first of a
1697
+ # name lets a project agent override its global namesake.
1698
+ result: dict[str, dict] = {}
1699
+ for found_name, path, scope in settings.get_all_agents():
1700
+ content = agent_md(path).read_text(encoding="utf-8", errors="replace")
1701
+ name = _extract_frontmatter_field(content, "name") or found_name
1702
+ description = _extract_frontmatter_field(content, "description") or ""
1703
+ result.setdefault(
1704
+ name,
1705
+ {
1706
+ "name": name,
1707
+ "description": description[:120],
1708
+ "source": scope,
1709
+ "path": str(path),
1710
+ },
1711
+ )
1712
+ return list(result.values())
1713
+
1714
+ def _get_agent(self, name: str) -> dict | None:
1715
+ """Get an agent's full file content by name (project first, then global).
1716
+
1717
+ Returns the agent dict, or None if not found (caller sends 404).
1718
+ """
1719
+ settings = self.settings
1720
+ found = settings.find_agent(name) if settings else None
1721
+ if not found:
1722
+ return None
1723
+ path, scope = found
1724
+ content = agent_md(path).read_text(encoding="utf-8", errors="replace")
1725
+ return {"name": name, "content": content, "source": scope, "path": str(path)}
1726
+
1727
+ def _create_agent(self, body: dict) -> None:
1728
+ """Create a new agent as a flat ``<name>.md`` file."""
1729
+ name = body.get("name", "").strip()
1730
+ description = body.get("description", "").strip()
1731
+ content = body.get("content", "")
1732
+ scope = body.get("scope", "global")
1733
+
1734
+ if not name:
1735
+ self._send_error("Name is required")
1736
+ return
1737
+ if not _is_valid_name(name):
1738
+ self._send_error("Invalid name. Use only letters, numbers, hyphens, and underscores.")
1739
+ return
1740
+
1741
+ settings = self.settings
1742
+ if not settings:
1743
+ self._send_error("Settings not available", 500)
1744
+ return
1745
+
1746
+ # Determine target directory
1747
+ if scope == "project":
1748
+ base_dir = settings.get_project_agents_dir()
1749
+ if not base_dir:
1750
+ self._send_error("Not in a project directory", 400)
1751
+ return
1752
+ else:
1753
+ base_dir = settings.get_agents_root_dir()
1754
+
1755
+ target = base_dir / f"{name}.md"
1756
+ if target.exists() or (base_dir / name).exists():
1757
+ self._send_error(f"Agent '{name}' already exists", 409)
1758
+ return
1759
+
1760
+ # Ensure content has frontmatter
1761
+ final_content = _ensure_frontmatter(content, name, description)
1762
+
1763
+ base_dir.mkdir(parents=True, exist_ok=True)
1764
+ if not self._write_atomically(target, final_content, "Failed to create agent"):
1765
+ return
1766
+
1767
+ self._send_json({"name": name, "source": scope, "path": str(target)}, 201)
1768
+
1769
+ def _write_atomically(self, target: Path, content: str, failure: str) -> bool:
1770
+ """Replace *target* with *content* via a temp file; send a 500 on failure."""
1771
+ tmp_path = target.parent / f".tmp.{target.name}.{os.getpid()}"
1772
+ try:
1773
+ tmp_path.write_text(content, encoding="utf-8")
1774
+ tmp_path.replace(target)
1775
+ except Exception as e:
1776
+ tmp_path.unlink(missing_ok=True)
1777
+ self._send_error(f"{failure}: {e}", 500)
1778
+ return False
1779
+ return True
1780
+
1781
+ def _update_agent(self, name: str, body: dict) -> None:
1782
+ """Update an agent's file content."""
1783
+ content = body.get("content", "")
1784
+ if not content:
1785
+ self._send_error("Content is required")
1786
+ return
1787
+
1788
+ settings = self.settings
1789
+ if not settings:
1790
+ self._send_error("Settings not available", 500)
1791
+ return
1792
+
1793
+ found = settings.find_agent(name)
1794
+ if not found:
1795
+ self._send_error(f"Agent '{name}' not found", 404)
1796
+ return
1797
+
1798
+ if not self._write_atomically(agent_md(found[0]), content, "Failed to save"):
1799
+ return
1800
+
1801
+ self._send_json({"name": name, "status": "saved"})
1802
+
1803
+ def _delete_agent(self, name: str) -> None:
1804
+ """Delete an agent (its flat file, or its folder)."""
1805
+ settings = self.settings
1806
+ if not settings:
1807
+ self._send_error("Settings not available", 500)
1808
+ return
1809
+
1810
+ import shutil
1811
+
1812
+ found = settings.find_agent(name)
1813
+ if not found:
1814
+ self._send_error(f"Agent '{name}' not found", 404)
1815
+ return
1816
+
1817
+ path = found[0]
1818
+ try:
1819
+ path.unlink() if path.is_file() else shutil.rmtree(path)
1820
+ except Exception as e:
1821
+ self._send_error(f"Failed to delete: {e}", 500)
1822
+ return
1823
+
1824
+ self._send_json({"name": name, "status": "deleted"})
1825
+
1826
+
1827
+ # ---------------------------------------------------------------------------
1828
+ # Server
1829
+ # ---------------------------------------------------------------------------
1830
+
1831
+
1832
+ class CreateServer:
1833
+ """Lightweight HTTP server for the Create web UI.
1834
+
1835
+ Runs in a background daemon thread. Serves a self-contained HTML single-page
1836
+ app for browsing, previewing, editing, creating, and deleting skills and agents.
1837
+ """
1838
+
1839
+ def __init__(self) -> None:
1840
+ self._server: HTTPServer | None = None
1841
+ self._thread: threading.Thread | None = None
1842
+ self.port: int = 0
1843
+ self.is_running: bool = False
1844
+
1845
+ def _find_available_port(self) -> int:
1846
+ """Find a random available port on localhost."""
1847
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
1848
+ s.bind(("127.0.0.1", 0))
1849
+ return s.getsockname()[1]
1850
+
1851
+ async def start(self) -> int:
1852
+ """Start the HTTP server in a background daemon thread.
1853
+
1854
+ Returns:
1855
+ The port number the server is listening on.
1856
+ """
1857
+ port = self._find_available_port()
1858
+
1859
+ # Wire up shared state to the handler class
1860
+ CreateRequestHandler.settings = Settings.from_environment()
1861
+
1862
+ self._server = HTTPServer(("127.0.0.1", port), CreateRequestHandler)
1863
+ import secrets
1864
+ self._server.auth_token = secrets.token_urlsafe(32)
1865
+ self.port = port
1866
+ self.is_running = True
1867
+
1868
+ self._thread = threading.Thread(
1869
+ target=self._server.serve_forever,
1870
+ daemon=True,
1871
+ name="create-server",
1872
+ )
1873
+ self._thread.start()
1874
+
1875
+ return port
1876
+
1877
+ @property
1878
+ def url(self) -> str:
1879
+ return f"http://localhost:{self.port}/?token={self._server.auth_token}" if self._server else ""
1880
+
1881
+ def stop(self) -> None:
1882
+ """Stop the HTTP server."""
1883
+ self.is_running = False
1884
+ if self._server is not None:
1885
+ self._server.shutdown()
1886
+ self._server.server_close()
1887
+ self._server = None