PraisonAI 3.0.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 (393) hide show
  1. praisonai/__init__.py +54 -0
  2. praisonai/__main__.py +15 -0
  3. praisonai/acp/__init__.py +54 -0
  4. praisonai/acp/config.py +159 -0
  5. praisonai/acp/server.py +587 -0
  6. praisonai/acp/session.py +219 -0
  7. praisonai/adapters/__init__.py +50 -0
  8. praisonai/adapters/readers.py +395 -0
  9. praisonai/adapters/rerankers.py +315 -0
  10. praisonai/adapters/retrievers.py +394 -0
  11. praisonai/adapters/vector_stores.py +409 -0
  12. praisonai/agent_scheduler.py +337 -0
  13. praisonai/agents_generator.py +903 -0
  14. praisonai/api/call.py +292 -0
  15. praisonai/auto.py +1197 -0
  16. praisonai/capabilities/__init__.py +275 -0
  17. praisonai/capabilities/a2a.py +140 -0
  18. praisonai/capabilities/assistants.py +283 -0
  19. praisonai/capabilities/audio.py +320 -0
  20. praisonai/capabilities/batches.py +469 -0
  21. praisonai/capabilities/completions.py +336 -0
  22. praisonai/capabilities/container_files.py +155 -0
  23. praisonai/capabilities/containers.py +93 -0
  24. praisonai/capabilities/embeddings.py +158 -0
  25. praisonai/capabilities/files.py +467 -0
  26. praisonai/capabilities/fine_tuning.py +293 -0
  27. praisonai/capabilities/guardrails.py +182 -0
  28. praisonai/capabilities/images.py +330 -0
  29. praisonai/capabilities/mcp.py +190 -0
  30. praisonai/capabilities/messages.py +270 -0
  31. praisonai/capabilities/moderations.py +154 -0
  32. praisonai/capabilities/ocr.py +217 -0
  33. praisonai/capabilities/passthrough.py +204 -0
  34. praisonai/capabilities/rag.py +207 -0
  35. praisonai/capabilities/realtime.py +160 -0
  36. praisonai/capabilities/rerank.py +165 -0
  37. praisonai/capabilities/responses.py +266 -0
  38. praisonai/capabilities/search.py +109 -0
  39. praisonai/capabilities/skills.py +133 -0
  40. praisonai/capabilities/vector_store_files.py +334 -0
  41. praisonai/capabilities/vector_stores.py +304 -0
  42. praisonai/capabilities/videos.py +141 -0
  43. praisonai/chainlit_ui.py +304 -0
  44. praisonai/chat/__init__.py +106 -0
  45. praisonai/chat/app.py +125 -0
  46. praisonai/cli/__init__.py +26 -0
  47. praisonai/cli/app.py +213 -0
  48. praisonai/cli/commands/__init__.py +75 -0
  49. praisonai/cli/commands/acp.py +70 -0
  50. praisonai/cli/commands/completion.py +333 -0
  51. praisonai/cli/commands/config.py +166 -0
  52. praisonai/cli/commands/debug.py +142 -0
  53. praisonai/cli/commands/diag.py +55 -0
  54. praisonai/cli/commands/doctor.py +166 -0
  55. praisonai/cli/commands/environment.py +179 -0
  56. praisonai/cli/commands/lsp.py +112 -0
  57. praisonai/cli/commands/mcp.py +210 -0
  58. praisonai/cli/commands/profile.py +457 -0
  59. praisonai/cli/commands/run.py +228 -0
  60. praisonai/cli/commands/schedule.py +150 -0
  61. praisonai/cli/commands/serve.py +97 -0
  62. praisonai/cli/commands/session.py +212 -0
  63. praisonai/cli/commands/traces.py +145 -0
  64. praisonai/cli/commands/version.py +101 -0
  65. praisonai/cli/configuration/__init__.py +18 -0
  66. praisonai/cli/configuration/loader.py +353 -0
  67. praisonai/cli/configuration/paths.py +114 -0
  68. praisonai/cli/configuration/schema.py +164 -0
  69. praisonai/cli/features/__init__.py +268 -0
  70. praisonai/cli/features/acp.py +236 -0
  71. praisonai/cli/features/action_orchestrator.py +546 -0
  72. praisonai/cli/features/agent_scheduler.py +773 -0
  73. praisonai/cli/features/agent_tools.py +474 -0
  74. praisonai/cli/features/agents.py +375 -0
  75. praisonai/cli/features/at_mentions.py +471 -0
  76. praisonai/cli/features/auto_memory.py +182 -0
  77. praisonai/cli/features/autonomy_mode.py +490 -0
  78. praisonai/cli/features/background.py +356 -0
  79. praisonai/cli/features/base.py +168 -0
  80. praisonai/cli/features/capabilities.py +1326 -0
  81. praisonai/cli/features/checkpoints.py +338 -0
  82. praisonai/cli/features/code_intelligence.py +652 -0
  83. praisonai/cli/features/compaction.py +294 -0
  84. praisonai/cli/features/compare.py +534 -0
  85. praisonai/cli/features/cost_tracker.py +514 -0
  86. praisonai/cli/features/debug.py +810 -0
  87. praisonai/cli/features/deploy.py +517 -0
  88. praisonai/cli/features/diag.py +289 -0
  89. praisonai/cli/features/doctor/__init__.py +63 -0
  90. praisonai/cli/features/doctor/checks/__init__.py +24 -0
  91. praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
  92. praisonai/cli/features/doctor/checks/config_checks.py +366 -0
  93. praisonai/cli/features/doctor/checks/db_checks.py +366 -0
  94. praisonai/cli/features/doctor/checks/env_checks.py +543 -0
  95. praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
  96. praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
  97. praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
  98. praisonai/cli/features/doctor/checks/network_checks.py +251 -0
  99. praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
  100. praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
  101. praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
  102. praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
  103. praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
  104. praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
  105. praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
  106. praisonai/cli/features/doctor/engine.py +266 -0
  107. praisonai/cli/features/doctor/formatters.py +310 -0
  108. praisonai/cli/features/doctor/handler.py +397 -0
  109. praisonai/cli/features/doctor/models.py +264 -0
  110. praisonai/cli/features/doctor/registry.py +239 -0
  111. praisonai/cli/features/endpoints.py +1019 -0
  112. praisonai/cli/features/eval.py +560 -0
  113. praisonai/cli/features/external_agents.py +231 -0
  114. praisonai/cli/features/fast_context.py +410 -0
  115. praisonai/cli/features/flow_display.py +566 -0
  116. praisonai/cli/features/git_integration.py +651 -0
  117. praisonai/cli/features/guardrail.py +171 -0
  118. praisonai/cli/features/handoff.py +185 -0
  119. praisonai/cli/features/hooks.py +583 -0
  120. praisonai/cli/features/image.py +384 -0
  121. praisonai/cli/features/interactive_runtime.py +585 -0
  122. praisonai/cli/features/interactive_tools.py +380 -0
  123. praisonai/cli/features/interactive_tui.py +603 -0
  124. praisonai/cli/features/jobs.py +632 -0
  125. praisonai/cli/features/knowledge.py +531 -0
  126. praisonai/cli/features/lite.py +244 -0
  127. praisonai/cli/features/lsp_cli.py +225 -0
  128. praisonai/cli/features/mcp.py +169 -0
  129. praisonai/cli/features/message_queue.py +587 -0
  130. praisonai/cli/features/metrics.py +211 -0
  131. praisonai/cli/features/n8n.py +673 -0
  132. praisonai/cli/features/observability.py +293 -0
  133. praisonai/cli/features/ollama.py +361 -0
  134. praisonai/cli/features/output_style.py +273 -0
  135. praisonai/cli/features/package.py +631 -0
  136. praisonai/cli/features/performance.py +308 -0
  137. praisonai/cli/features/persistence.py +636 -0
  138. praisonai/cli/features/profile.py +226 -0
  139. praisonai/cli/features/profiler/__init__.py +81 -0
  140. praisonai/cli/features/profiler/core.py +558 -0
  141. praisonai/cli/features/profiler/optimizations.py +652 -0
  142. praisonai/cli/features/profiler/suite.py +386 -0
  143. praisonai/cli/features/profiling.py +350 -0
  144. praisonai/cli/features/queue/__init__.py +73 -0
  145. praisonai/cli/features/queue/manager.py +395 -0
  146. praisonai/cli/features/queue/models.py +286 -0
  147. praisonai/cli/features/queue/persistence.py +564 -0
  148. praisonai/cli/features/queue/scheduler.py +484 -0
  149. praisonai/cli/features/queue/worker.py +372 -0
  150. praisonai/cli/features/recipe.py +1723 -0
  151. praisonai/cli/features/recipes.py +449 -0
  152. praisonai/cli/features/registry.py +229 -0
  153. praisonai/cli/features/repo_map.py +860 -0
  154. praisonai/cli/features/router.py +466 -0
  155. praisonai/cli/features/sandbox_executor.py +515 -0
  156. praisonai/cli/features/serve.py +829 -0
  157. praisonai/cli/features/session.py +222 -0
  158. praisonai/cli/features/skills.py +856 -0
  159. praisonai/cli/features/slash_commands.py +650 -0
  160. praisonai/cli/features/telemetry.py +179 -0
  161. praisonai/cli/features/templates.py +1384 -0
  162. praisonai/cli/features/thinking.py +305 -0
  163. praisonai/cli/features/todo.py +334 -0
  164. praisonai/cli/features/tools.py +680 -0
  165. praisonai/cli/features/tui/__init__.py +83 -0
  166. praisonai/cli/features/tui/app.py +580 -0
  167. praisonai/cli/features/tui/cli.py +566 -0
  168. praisonai/cli/features/tui/debug.py +511 -0
  169. praisonai/cli/features/tui/events.py +99 -0
  170. praisonai/cli/features/tui/mock_provider.py +328 -0
  171. praisonai/cli/features/tui/orchestrator.py +652 -0
  172. praisonai/cli/features/tui/screens/__init__.py +50 -0
  173. praisonai/cli/features/tui/screens/main.py +245 -0
  174. praisonai/cli/features/tui/screens/queue.py +174 -0
  175. praisonai/cli/features/tui/screens/session.py +124 -0
  176. praisonai/cli/features/tui/screens/settings.py +148 -0
  177. praisonai/cli/features/tui/widgets/__init__.py +56 -0
  178. praisonai/cli/features/tui/widgets/chat.py +261 -0
  179. praisonai/cli/features/tui/widgets/composer.py +224 -0
  180. praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
  181. praisonai/cli/features/tui/widgets/status.py +167 -0
  182. praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
  183. praisonai/cli/features/workflow.py +720 -0
  184. praisonai/cli/legacy.py +236 -0
  185. praisonai/cli/main.py +5559 -0
  186. praisonai/cli/schedule_cli.py +54 -0
  187. praisonai/cli/state/__init__.py +31 -0
  188. praisonai/cli/state/identifiers.py +161 -0
  189. praisonai/cli/state/sessions.py +313 -0
  190. praisonai/code/__init__.py +93 -0
  191. praisonai/code/agent_tools.py +344 -0
  192. praisonai/code/diff/__init__.py +21 -0
  193. praisonai/code/diff/diff_strategy.py +432 -0
  194. praisonai/code/tools/__init__.py +27 -0
  195. praisonai/code/tools/apply_diff.py +221 -0
  196. praisonai/code/tools/execute_command.py +275 -0
  197. praisonai/code/tools/list_files.py +274 -0
  198. praisonai/code/tools/read_file.py +206 -0
  199. praisonai/code/tools/search_replace.py +248 -0
  200. praisonai/code/tools/write_file.py +217 -0
  201. praisonai/code/utils/__init__.py +46 -0
  202. praisonai/code/utils/file_utils.py +307 -0
  203. praisonai/code/utils/ignore_utils.py +308 -0
  204. praisonai/code/utils/text_utils.py +276 -0
  205. praisonai/db/__init__.py +64 -0
  206. praisonai/db/adapter.py +531 -0
  207. praisonai/deploy/__init__.py +62 -0
  208. praisonai/deploy/api.py +231 -0
  209. praisonai/deploy/docker.py +454 -0
  210. praisonai/deploy/doctor.py +367 -0
  211. praisonai/deploy/main.py +327 -0
  212. praisonai/deploy/models.py +179 -0
  213. praisonai/deploy/providers/__init__.py +33 -0
  214. praisonai/deploy/providers/aws.py +331 -0
  215. praisonai/deploy/providers/azure.py +358 -0
  216. praisonai/deploy/providers/base.py +101 -0
  217. praisonai/deploy/providers/gcp.py +314 -0
  218. praisonai/deploy/schema.py +208 -0
  219. praisonai/deploy.py +185 -0
  220. praisonai/endpoints/__init__.py +53 -0
  221. praisonai/endpoints/a2u_server.py +410 -0
  222. praisonai/endpoints/discovery.py +165 -0
  223. praisonai/endpoints/providers/__init__.py +28 -0
  224. praisonai/endpoints/providers/a2a.py +253 -0
  225. praisonai/endpoints/providers/a2u.py +208 -0
  226. praisonai/endpoints/providers/agents_api.py +171 -0
  227. praisonai/endpoints/providers/base.py +231 -0
  228. praisonai/endpoints/providers/mcp.py +263 -0
  229. praisonai/endpoints/providers/recipe.py +206 -0
  230. praisonai/endpoints/providers/tools_mcp.py +150 -0
  231. praisonai/endpoints/registry.py +131 -0
  232. praisonai/endpoints/server.py +161 -0
  233. praisonai/inbuilt_tools/__init__.py +24 -0
  234. praisonai/inbuilt_tools/autogen_tools.py +117 -0
  235. praisonai/inc/__init__.py +2 -0
  236. praisonai/inc/config.py +96 -0
  237. praisonai/inc/models.py +155 -0
  238. praisonai/integrations/__init__.py +56 -0
  239. praisonai/integrations/base.py +303 -0
  240. praisonai/integrations/claude_code.py +270 -0
  241. praisonai/integrations/codex_cli.py +255 -0
  242. praisonai/integrations/cursor_cli.py +195 -0
  243. praisonai/integrations/gemini_cli.py +222 -0
  244. praisonai/jobs/__init__.py +67 -0
  245. praisonai/jobs/executor.py +425 -0
  246. praisonai/jobs/models.py +230 -0
  247. praisonai/jobs/router.py +314 -0
  248. praisonai/jobs/server.py +186 -0
  249. praisonai/jobs/store.py +203 -0
  250. praisonai/llm/__init__.py +66 -0
  251. praisonai/llm/registry.py +382 -0
  252. praisonai/mcp_server/__init__.py +152 -0
  253. praisonai/mcp_server/adapters/__init__.py +74 -0
  254. praisonai/mcp_server/adapters/agents.py +128 -0
  255. praisonai/mcp_server/adapters/capabilities.py +168 -0
  256. praisonai/mcp_server/adapters/cli_tools.py +568 -0
  257. praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
  258. praisonai/mcp_server/adapters/knowledge.py +93 -0
  259. praisonai/mcp_server/adapters/memory.py +104 -0
  260. praisonai/mcp_server/adapters/prompts.py +306 -0
  261. praisonai/mcp_server/adapters/resources.py +124 -0
  262. praisonai/mcp_server/adapters/tools_bridge.py +280 -0
  263. praisonai/mcp_server/auth/__init__.py +48 -0
  264. praisonai/mcp_server/auth/api_key.py +291 -0
  265. praisonai/mcp_server/auth/oauth.py +460 -0
  266. praisonai/mcp_server/auth/oidc.py +289 -0
  267. praisonai/mcp_server/auth/scopes.py +260 -0
  268. praisonai/mcp_server/cli.py +852 -0
  269. praisonai/mcp_server/elicitation.py +445 -0
  270. praisonai/mcp_server/icons.py +302 -0
  271. praisonai/mcp_server/recipe_adapter.py +573 -0
  272. praisonai/mcp_server/recipe_cli.py +824 -0
  273. praisonai/mcp_server/registry.py +703 -0
  274. praisonai/mcp_server/sampling.py +422 -0
  275. praisonai/mcp_server/server.py +490 -0
  276. praisonai/mcp_server/tasks.py +443 -0
  277. praisonai/mcp_server/transports/__init__.py +18 -0
  278. praisonai/mcp_server/transports/http_stream.py +376 -0
  279. praisonai/mcp_server/transports/stdio.py +132 -0
  280. praisonai/persistence/__init__.py +84 -0
  281. praisonai/persistence/config.py +238 -0
  282. praisonai/persistence/conversation/__init__.py +25 -0
  283. praisonai/persistence/conversation/async_mysql.py +427 -0
  284. praisonai/persistence/conversation/async_postgres.py +410 -0
  285. praisonai/persistence/conversation/async_sqlite.py +371 -0
  286. praisonai/persistence/conversation/base.py +151 -0
  287. praisonai/persistence/conversation/json_store.py +250 -0
  288. praisonai/persistence/conversation/mysql.py +387 -0
  289. praisonai/persistence/conversation/postgres.py +401 -0
  290. praisonai/persistence/conversation/singlestore.py +240 -0
  291. praisonai/persistence/conversation/sqlite.py +341 -0
  292. praisonai/persistence/conversation/supabase.py +203 -0
  293. praisonai/persistence/conversation/surrealdb.py +287 -0
  294. praisonai/persistence/factory.py +301 -0
  295. praisonai/persistence/hooks/__init__.py +18 -0
  296. praisonai/persistence/hooks/agent_hooks.py +297 -0
  297. praisonai/persistence/knowledge/__init__.py +26 -0
  298. praisonai/persistence/knowledge/base.py +144 -0
  299. praisonai/persistence/knowledge/cassandra.py +232 -0
  300. praisonai/persistence/knowledge/chroma.py +295 -0
  301. praisonai/persistence/knowledge/clickhouse.py +242 -0
  302. praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
  303. praisonai/persistence/knowledge/couchbase.py +286 -0
  304. praisonai/persistence/knowledge/lancedb.py +216 -0
  305. praisonai/persistence/knowledge/langchain_adapter.py +291 -0
  306. praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
  307. praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
  308. praisonai/persistence/knowledge/milvus.py +277 -0
  309. praisonai/persistence/knowledge/mongodb_vector.py +306 -0
  310. praisonai/persistence/knowledge/pgvector.py +335 -0
  311. praisonai/persistence/knowledge/pinecone.py +253 -0
  312. praisonai/persistence/knowledge/qdrant.py +301 -0
  313. praisonai/persistence/knowledge/redis_vector.py +291 -0
  314. praisonai/persistence/knowledge/singlestore_vector.py +299 -0
  315. praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
  316. praisonai/persistence/knowledge/upstash_vector.py +266 -0
  317. praisonai/persistence/knowledge/weaviate.py +223 -0
  318. praisonai/persistence/migrations/__init__.py +10 -0
  319. praisonai/persistence/migrations/manager.py +251 -0
  320. praisonai/persistence/orchestrator.py +406 -0
  321. praisonai/persistence/state/__init__.py +21 -0
  322. praisonai/persistence/state/async_mongodb.py +200 -0
  323. praisonai/persistence/state/base.py +107 -0
  324. praisonai/persistence/state/dynamodb.py +226 -0
  325. praisonai/persistence/state/firestore.py +175 -0
  326. praisonai/persistence/state/gcs.py +155 -0
  327. praisonai/persistence/state/memory.py +245 -0
  328. praisonai/persistence/state/mongodb.py +158 -0
  329. praisonai/persistence/state/redis.py +190 -0
  330. praisonai/persistence/state/upstash.py +144 -0
  331. praisonai/persistence/tests/__init__.py +3 -0
  332. praisonai/persistence/tests/test_all_backends.py +633 -0
  333. praisonai/profiler.py +1214 -0
  334. praisonai/recipe/__init__.py +134 -0
  335. praisonai/recipe/bridge.py +278 -0
  336. praisonai/recipe/core.py +893 -0
  337. praisonai/recipe/exceptions.py +54 -0
  338. praisonai/recipe/history.py +402 -0
  339. praisonai/recipe/models.py +266 -0
  340. praisonai/recipe/operations.py +440 -0
  341. praisonai/recipe/policy.py +422 -0
  342. praisonai/recipe/registry.py +849 -0
  343. praisonai/recipe/runtime.py +214 -0
  344. praisonai/recipe/security.py +711 -0
  345. praisonai/recipe/serve.py +859 -0
  346. praisonai/recipe/server.py +613 -0
  347. praisonai/scheduler/__init__.py +45 -0
  348. praisonai/scheduler/agent_scheduler.py +552 -0
  349. praisonai/scheduler/base.py +124 -0
  350. praisonai/scheduler/daemon_manager.py +225 -0
  351. praisonai/scheduler/state_manager.py +155 -0
  352. praisonai/scheduler/yaml_loader.py +193 -0
  353. praisonai/scheduler.py +194 -0
  354. praisonai/setup/__init__.py +1 -0
  355. praisonai/setup/build.py +21 -0
  356. praisonai/setup/post_install.py +23 -0
  357. praisonai/setup/setup_conda_env.py +25 -0
  358. praisonai/setup.py +16 -0
  359. praisonai/templates/__init__.py +116 -0
  360. praisonai/templates/cache.py +364 -0
  361. praisonai/templates/dependency_checker.py +358 -0
  362. praisonai/templates/discovery.py +391 -0
  363. praisonai/templates/loader.py +564 -0
  364. praisonai/templates/registry.py +511 -0
  365. praisonai/templates/resolver.py +206 -0
  366. praisonai/templates/security.py +327 -0
  367. praisonai/templates/tool_override.py +498 -0
  368. praisonai/templates/tools_doctor.py +256 -0
  369. praisonai/test.py +105 -0
  370. praisonai/train.py +562 -0
  371. praisonai/train_vision.py +306 -0
  372. praisonai/ui/agents.py +824 -0
  373. praisonai/ui/callbacks.py +57 -0
  374. praisonai/ui/chainlit_compat.py +246 -0
  375. praisonai/ui/chat.py +532 -0
  376. praisonai/ui/code.py +717 -0
  377. praisonai/ui/colab.py +474 -0
  378. praisonai/ui/colab_chainlit.py +81 -0
  379. praisonai/ui/components/aicoder.py +284 -0
  380. praisonai/ui/context.py +283 -0
  381. praisonai/ui/database_config.py +56 -0
  382. praisonai/ui/db.py +294 -0
  383. praisonai/ui/realtime.py +488 -0
  384. praisonai/ui/realtimeclient/__init__.py +756 -0
  385. praisonai/ui/realtimeclient/tools.py +242 -0
  386. praisonai/ui/sql_alchemy.py +710 -0
  387. praisonai/upload_vision.py +140 -0
  388. praisonai/version.py +1 -0
  389. praisonai-3.0.0.dist-info/METADATA +3493 -0
  390. praisonai-3.0.0.dist-info/RECORD +393 -0
  391. praisonai-3.0.0.dist-info/WHEEL +5 -0
  392. praisonai-3.0.0.dist-info/entry_points.txt +4 -0
  393. praisonai-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,631 @@
1
+ """
2
+ Package CLI Feature Handler
3
+
4
+ Provides pip-like CLI commands for package management:
5
+ - install: Install Python packages
6
+ - uninstall: Uninstall Python packages
7
+ - list: List installed packages
8
+ - search: Search packages
9
+ - index: Manage package indexes
10
+
11
+ All commands use the canonical `praisonai` or `praisonai package` prefix.
12
+
13
+ Security:
14
+ - Default to single authoritative index (PyPI)
15
+ - Warn loudly about dependency confusion when using extra indexes
16
+ - Require explicit opt-in for extra index fallback
17
+ """
18
+
19
+ import json
20
+ import os
21
+ import subprocess
22
+ import sys
23
+ from pathlib import Path
24
+ from typing import Any, Dict, List, Tuple
25
+
26
+
27
+ # Default configuration
28
+ DEFAULT_INDEX_URL = "https://pypi.org/simple"
29
+ CONFIG_DIR = Path.home() / ".praison"
30
+ CONFIG_FILE = CONFIG_DIR / "config.toml"
31
+
32
+
33
+ class PackageConfig:
34
+ """Package configuration manager."""
35
+
36
+ def __init__(self):
37
+ """Initialize config."""
38
+ self._config = None
39
+
40
+ def _load_config(self) -> Dict[str, Any]:
41
+ """Load configuration from file."""
42
+ if self._config is not None:
43
+ return self._config
44
+
45
+ self._config = {
46
+ "package": {
47
+ "index_url": DEFAULT_INDEX_URL,
48
+ "extra_index_urls": [],
49
+ "allow_extra_index": False,
50
+ "trusted_hosts": [],
51
+ }
52
+ }
53
+
54
+ if CONFIG_FILE.exists():
55
+ try:
56
+ import tomllib
57
+ except ImportError:
58
+ try:
59
+ import tomli as tomllib
60
+ except ImportError:
61
+ return self._config
62
+
63
+ try:
64
+ with open(CONFIG_FILE, "rb") as f:
65
+ file_config = tomllib.load(f)
66
+ if "package" in file_config:
67
+ self._config["package"].update(file_config["package"])
68
+ except Exception:
69
+ pass
70
+
71
+ # Environment variable overrides
72
+ if os.environ.get("PRAISONAI_PACKAGE_INDEX_URL"):
73
+ self._config["package"]["index_url"] = os.environ["PRAISONAI_PACKAGE_INDEX_URL"]
74
+
75
+ if os.environ.get("PRAISONAI_PACKAGE_EXTRA_INDEX_URLS"):
76
+ urls = os.environ["PRAISONAI_PACKAGE_EXTRA_INDEX_URLS"].split(",")
77
+ self._config["package"]["extra_index_urls"] = [u.strip() for u in urls if u.strip()]
78
+
79
+ return self._config
80
+
81
+ def get(self, key: str, default: Any = None) -> Any:
82
+ """Get config value."""
83
+ config = self._load_config()
84
+ parts = key.split(".")
85
+ value = config
86
+ for part in parts:
87
+ if isinstance(value, dict) and part in value:
88
+ value = value[part]
89
+ else:
90
+ return default
91
+ return value
92
+
93
+ def save_index(self, index_url: str) -> None:
94
+ """Save index URL to config."""
95
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
96
+
97
+ config = {}
98
+ if CONFIG_FILE.exists():
99
+ try:
100
+ import tomllib
101
+ except ImportError:
102
+ try:
103
+ import tomli as tomllib
104
+ except ImportError:
105
+ # Fall back to simple write
106
+ with open(CONFIG_FILE, "w") as f:
107
+ f.write(f'[package]\nindex_url = "{index_url}"\n')
108
+ return
109
+
110
+ try:
111
+ with open(CONFIG_FILE, "rb") as f:
112
+ config = tomllib.load(f)
113
+ except Exception:
114
+ pass
115
+
116
+ if "package" not in config:
117
+ config["package"] = {}
118
+ config["package"]["index_url"] = index_url
119
+
120
+ # Write config (simple TOML format)
121
+ with open(CONFIG_FILE, "w") as f:
122
+ for section, values in config.items():
123
+ f.write(f"[{section}]\n")
124
+ for key, value in values.items():
125
+ if isinstance(value, str):
126
+ f.write(f'{key} = "{value}"\n')
127
+ elif isinstance(value, bool):
128
+ f.write(f'{key} = {"true" if value else "false"}\n')
129
+ elif isinstance(value, list):
130
+ f.write(f'{key} = {json.dumps(value)}\n')
131
+ else:
132
+ f.write(f'{key} = {value}\n')
133
+ f.write("\n")
134
+
135
+
136
+ class PackageHandler:
137
+ """
138
+ CLI handler for package operations.
139
+
140
+ Commands:
141
+ - install: Install Python packages
142
+ - uninstall: Uninstall Python packages
143
+ - list: List installed packages
144
+ - search: Search packages (best-effort)
145
+ - index: Manage package indexes
146
+ """
147
+
148
+ # Stable exit codes
149
+ EXIT_SUCCESS = 0
150
+ EXIT_GENERAL_ERROR = 1
151
+ EXIT_VALIDATION_ERROR = 2
152
+ EXIT_NETWORK_ERROR = 10
153
+ EXIT_DEPENDENCY_ERROR = 11
154
+
155
+ def __init__(self):
156
+ """Initialize the handler."""
157
+ self.config = PackageConfig()
158
+
159
+ def handle(self, args: List[str]) -> int:
160
+ """
161
+ Handle package subcommand.
162
+
163
+ Args:
164
+ args: Command arguments
165
+
166
+ Returns:
167
+ Exit code
168
+ """
169
+ if not args:
170
+ self._print_help()
171
+ return self.EXIT_SUCCESS
172
+
173
+ command = args[0]
174
+ remaining = args[1:]
175
+
176
+ commands = {
177
+ "list": self.cmd_list,
178
+ "search": self.cmd_search,
179
+ "index": self.cmd_index,
180
+ "help": lambda _: self._print_help() or self.EXIT_SUCCESS,
181
+ "--help": lambda _: self._print_help() or self.EXIT_SUCCESS,
182
+ "-h": lambda _: self._print_help() or self.EXIT_SUCCESS,
183
+ }
184
+
185
+ handler = commands.get(command)
186
+ if handler:
187
+ return handler(remaining)
188
+
189
+ self._print_error(f"Unknown command: {command}")
190
+ self._print_help()
191
+ return self.EXIT_VALIDATION_ERROR
192
+
193
+ def _print_help(self):
194
+ """Print help message."""
195
+ print("""
196
+ PraisonAI Package Commands
197
+
198
+ Usage: praisonai package <command> [options]
199
+ praisonai install <spec...>
200
+ praisonai uninstall <pkg...>
201
+
202
+ Commands:
203
+ install Install Python packages (shortcut: praisonai install)
204
+ uninstall Uninstall Python packages (shortcut: praisonai uninstall)
205
+ list List installed packages
206
+ search Search for packages
207
+ index Manage package indexes
208
+
209
+ Examples:
210
+ praisonai install requests
211
+ praisonai install "requests>=2.28" httpx
212
+ praisonai uninstall requests
213
+ praisonai package list
214
+ praisonai package search langchain
215
+ praisonai package index show
216
+ praisonai package index set https://my-index.example.com/simple
217
+
218
+ Options for 'install':
219
+ --index-url URL Use custom index URL
220
+ --extra-index-url URL Add extra index (requires --allow-extra-index)
221
+ --allow-extra-index Allow extra index URLs (security risk!)
222
+ --python PATH Python interpreter to use
223
+ --upgrade, -U Upgrade packages
224
+ --no-deps Don't install dependencies
225
+ --json Output in JSON format
226
+
227
+ Options for 'uninstall':
228
+ --python PATH Python interpreter to use
229
+ --yes, -y Don't ask for confirmation
230
+ --json Output in JSON format
231
+
232
+ Security Notes:
233
+ - By default, only the primary index is used (PyPI)
234
+ - Using --extra-index-url can lead to dependency confusion attacks
235
+ - Always prefer --index-url over --extra-index-url when possible
236
+ """)
237
+
238
+ def _print_error(self, message: str):
239
+ """Print error message."""
240
+ print(f"Error: {message}", file=sys.stderr)
241
+
242
+ def _print_warning(self, message: str):
243
+ """Print warning message."""
244
+ print(f"Warning: {message}", file=sys.stderr)
245
+
246
+ def _print_success(self, message: str):
247
+ """Print success message."""
248
+ print(f"✓ {message}")
249
+
250
+ def _print_json(self, data: Any):
251
+ """Print JSON output."""
252
+ print(json.dumps(data, indent=2))
253
+
254
+ def _parse_args(self, args: List[str], spec: Dict[str, Any]) -> Tuple[Dict[str, Any], List[str]]:
255
+ """Parse command arguments, returning parsed args and positional args."""
256
+ result = {k: v.get("default") for k, v in spec.items()}
257
+ positional = []
258
+
259
+ i = 0
260
+ while i < len(args):
261
+ arg = args[i]
262
+
263
+ if arg.startswith("--"):
264
+ key = arg[2:].replace("-", "_")
265
+ if key in spec:
266
+ if spec[key].get("flag"):
267
+ result[key] = True
268
+ elif i + 1 < len(args):
269
+ result[key] = args[i + 1]
270
+ i += 1
271
+ else:
272
+ positional.append(arg)
273
+ elif arg.startswith("-") and len(arg) == 2:
274
+ # Short flag
275
+ found = False
276
+ for key, val in spec.items():
277
+ if val.get("short") == arg:
278
+ if val.get("flag"):
279
+ result[key] = True
280
+ elif i + 1 < len(args):
281
+ result[key] = args[i + 1]
282
+ i += 1
283
+ found = True
284
+ break
285
+ if not found:
286
+ positional.append(arg)
287
+ else:
288
+ positional.append(arg)
289
+ i += 1
290
+
291
+ return result, positional
292
+
293
+ def _get_python(self, python_path: str = None) -> str:
294
+ """Get Python interpreter path."""
295
+ if python_path:
296
+ return python_path
297
+ return sys.executable
298
+
299
+ def _run_pip(
300
+ self,
301
+ args: List[str],
302
+ python: str = None,
303
+ capture: bool = False,
304
+ ) -> Tuple[int, str, str]:
305
+ """
306
+ Run pip command.
307
+
308
+ Args:
309
+ args: pip arguments
310
+ python: Python interpreter path
311
+ capture: Capture output instead of streaming
312
+
313
+ Returns:
314
+ Tuple of (return_code, stdout, stderr)
315
+ """
316
+ python = self._get_python(python)
317
+ cmd = [python, "-m", "pip"] + args
318
+
319
+ if capture:
320
+ result = subprocess.run(
321
+ cmd,
322
+ capture_output=True,
323
+ text=True,
324
+ env={**os.environ, "PAGER": "cat"},
325
+ )
326
+ return result.returncode, result.stdout, result.stderr
327
+ else:
328
+ result = subprocess.run(
329
+ cmd,
330
+ env={**os.environ, "PAGER": "cat"},
331
+ )
332
+ return result.returncode, "", ""
333
+
334
+ def cmd_install(self, args: List[str]) -> int:
335
+ """Install Python packages."""
336
+ spec = {
337
+ "index_url": {"default": None},
338
+ "extra_index_url": {"default": None},
339
+ "allow_extra_index": {"flag": True, "default": False},
340
+ "python": {"default": None},
341
+ "upgrade": {"flag": True, "short": "-U", "default": False},
342
+ "no_deps": {"flag": True, "default": False},
343
+ "json": {"flag": True, "default": False},
344
+ }
345
+ parsed, packages = self._parse_args(args, spec)
346
+
347
+ if not packages:
348
+ self._print_error("No packages specified")
349
+ return self.EXIT_VALIDATION_ERROR
350
+
351
+ # Build pip command
352
+ pip_args = ["install"]
353
+
354
+ # Index URL
355
+ index_url = parsed["index_url"] or self.config.get("package.index_url", DEFAULT_INDEX_URL)
356
+ pip_args.extend(["--index-url", index_url])
357
+
358
+ # Extra index URL (with security warning)
359
+ extra_index = parsed["extra_index_url"]
360
+ if extra_index:
361
+ allow_extra = parsed["allow_extra_index"] or self.config.get("package.allow_extra_index", False)
362
+ if not allow_extra:
363
+ self._print_error(
364
+ "Extra index URLs are disabled by default for security.\n"
365
+ "Using multiple indexes can lead to dependency confusion attacks.\n"
366
+ "To enable, use --allow-extra-index flag or set package.allow_extra_index=true in config."
367
+ )
368
+ return self.EXIT_VALIDATION_ERROR
369
+
370
+ self._print_warning(
371
+ "⚠️ SECURITY WARNING: Using extra index URLs can lead to dependency confusion attacks!\n"
372
+ " An attacker could publish a malicious package with the same name on PyPI.\n"
373
+ " Only use this if you trust both indexes and understand the risks."
374
+ )
375
+ pip_args.extend(["--extra-index-url", extra_index])
376
+
377
+ # Other options
378
+ if parsed["upgrade"]:
379
+ pip_args.append("--upgrade")
380
+ if parsed["no_deps"]:
381
+ pip_args.append("--no-deps")
382
+
383
+ # Add packages
384
+ pip_args.extend(packages)
385
+
386
+ if parsed["json"]:
387
+ returncode, stdout, stderr = self._run_pip(pip_args, parsed["python"], capture=True)
388
+ self._print_json({
389
+ "ok": returncode == 0,
390
+ "packages": packages,
391
+ "returncode": returncode,
392
+ "stdout": stdout,
393
+ "stderr": stderr,
394
+ })
395
+ return returncode
396
+ else:
397
+ print(f"Installing: {', '.join(packages)}")
398
+ returncode, _, _ = self._run_pip(pip_args, parsed["python"])
399
+ if returncode == 0:
400
+ self._print_success("Installation complete")
401
+ return returncode
402
+
403
+ def cmd_uninstall(self, args: List[str]) -> int:
404
+ """Uninstall Python packages."""
405
+ spec = {
406
+ "python": {"default": None},
407
+ "yes": {"flag": True, "short": "-y", "default": False},
408
+ "json": {"flag": True, "default": False},
409
+ }
410
+ parsed, packages = self._parse_args(args, spec)
411
+
412
+ if not packages:
413
+ self._print_error("No packages specified")
414
+ return self.EXIT_VALIDATION_ERROR
415
+
416
+ pip_args = ["uninstall"]
417
+ if parsed["yes"]:
418
+ pip_args.append("-y")
419
+ pip_args.extend(packages)
420
+
421
+ if parsed["json"]:
422
+ returncode, stdout, stderr = self._run_pip(pip_args, parsed["python"], capture=True)
423
+ self._print_json({
424
+ "ok": returncode == 0,
425
+ "packages": packages,
426
+ "returncode": returncode,
427
+ "stdout": stdout,
428
+ "stderr": stderr,
429
+ })
430
+ return returncode
431
+ else:
432
+ print(f"Uninstalling: {', '.join(packages)}")
433
+ returncode, _, _ = self._run_pip(pip_args, parsed["python"])
434
+ if returncode == 0:
435
+ self._print_success("Uninstall complete")
436
+ return returncode
437
+
438
+ def cmd_list(self, args: List[str]) -> int:
439
+ """List installed packages."""
440
+ spec = {
441
+ "python": {"default": None},
442
+ "json": {"flag": True, "default": False},
443
+ "outdated": {"flag": True, "default": False},
444
+ }
445
+ parsed, _ = self._parse_args(args, spec)
446
+
447
+ pip_args = ["list", "--format=json"]
448
+ if parsed["outdated"]:
449
+ pip_args.append("--outdated")
450
+
451
+ returncode, stdout, stderr = self._run_pip(pip_args, parsed["python"], capture=True)
452
+
453
+ if returncode != 0:
454
+ self._print_error(stderr or "Failed to list packages")
455
+ return returncode
456
+
457
+ try:
458
+ packages = json.loads(stdout)
459
+ except json.JSONDecodeError:
460
+ packages = []
461
+
462
+ if parsed["json"]:
463
+ self._print_json({
464
+ "ok": True,
465
+ "packages": packages,
466
+ "count": len(packages),
467
+ })
468
+ else:
469
+ if not packages:
470
+ print("No packages installed.")
471
+ else:
472
+ print(f"{'Package':<30} {'Version':<15}")
473
+ print("-" * 45)
474
+ for pkg in packages:
475
+ name = pkg.get("name", "")
476
+ version = pkg.get("version", "")
477
+ print(f"{name:<30} {version:<15}")
478
+ print(f"\nTotal: {len(packages)} packages")
479
+
480
+ return self.EXIT_SUCCESS
481
+
482
+ def cmd_search(self, args: List[str]) -> int:
483
+ """Search for packages (best-effort, uses pip search or PyPI API)."""
484
+ spec = {
485
+ "json": {"flag": True, "default": False},
486
+ }
487
+ parsed, query_parts = self._parse_args(args, spec)
488
+
489
+ if not query_parts:
490
+ self._print_error("Search query required")
491
+ return self.EXIT_VALIDATION_ERROR
492
+
493
+ query = " ".join(query_parts)
494
+
495
+ # pip search is deprecated, use PyPI JSON API
496
+ try:
497
+ import urllib.request
498
+ import urllib.error
499
+
500
+ url = f"https://pypi.org/pypi/{query}/json"
501
+ try:
502
+ with urllib.request.urlopen(url, timeout=10) as response:
503
+ data = json.loads(response.read().decode())
504
+
505
+ info = data.get("info", {})
506
+ result = {
507
+ "name": info.get("name"),
508
+ "version": info.get("version"),
509
+ "summary": info.get("summary"),
510
+ "author": info.get("author"),
511
+ "home_page": info.get("home_page"),
512
+ }
513
+
514
+ if parsed["json"]:
515
+ self._print_json({"ok": True, "results": [result]})
516
+ else:
517
+ print(f"Found: {result['name']} ({result['version']})")
518
+ print(f" {result['summary']}")
519
+ if result['home_page']:
520
+ print(f" Homepage: {result['home_page']}")
521
+
522
+ return self.EXIT_SUCCESS
523
+
524
+ except urllib.error.HTTPError as e:
525
+ if e.code == 404:
526
+ if parsed["json"]:
527
+ self._print_json({"ok": True, "results": [], "message": "No exact match found"})
528
+ else:
529
+ print(f"No exact match for '{query}'")
530
+ print("Try: pip search <query> (if available) or browse https://pypi.org")
531
+ return self.EXIT_SUCCESS
532
+ raise
533
+
534
+ except Exception as e:
535
+ if parsed["json"]:
536
+ self._print_json({"ok": False, "error": str(e)})
537
+ else:
538
+ self._print_error(f"Search failed: {e}")
539
+ return self.EXIT_NETWORK_ERROR
540
+
541
+ def cmd_index(self, args: List[str]) -> int:
542
+ """Manage package indexes."""
543
+ if not args:
544
+ return self._index_show([])
545
+
546
+ subcommand = args[0]
547
+ remaining = args[1:]
548
+
549
+ subcommands = {
550
+ "show": self._index_show,
551
+ "set": self._index_set,
552
+ "add": self._index_add,
553
+ "remove": self._index_remove,
554
+ }
555
+
556
+ handler = subcommands.get(subcommand)
557
+ if handler:
558
+ return handler(remaining)
559
+
560
+ self._print_error(f"Unknown index subcommand: {subcommand}")
561
+ return self.EXIT_VALIDATION_ERROR
562
+
563
+ def _index_show(self, args: List[str]) -> int:
564
+ """Show current index configuration."""
565
+ spec = {
566
+ "json": {"flag": True, "default": False},
567
+ }
568
+ parsed, _ = self._parse_args(args, spec)
569
+
570
+ index_url = self.config.get("package.index_url", DEFAULT_INDEX_URL)
571
+ extra_urls = self.config.get("package.extra_index_urls", [])
572
+ allow_extra = self.config.get("package.allow_extra_index", False)
573
+
574
+ if parsed["json"]:
575
+ self._print_json({
576
+ "ok": True,
577
+ "index_url": index_url,
578
+ "extra_index_urls": extra_urls,
579
+ "allow_extra_index": allow_extra,
580
+ })
581
+ else:
582
+ print(f"Primary Index: {index_url}")
583
+ if extra_urls:
584
+ print(f"Extra Indexes: {', '.join(extra_urls)}")
585
+ print(f"Allow Extra Index: {'yes' if allow_extra else 'no'}")
586
+
587
+ return self.EXIT_SUCCESS
588
+
589
+ def _index_set(self, args: List[str]) -> int:
590
+ """Set primary index URL."""
591
+ if not args:
592
+ self._print_error("Index URL required")
593
+ return self.EXIT_VALIDATION_ERROR
594
+
595
+ url = args[0]
596
+
597
+ # Validate URL format
598
+ if not url.startswith(("http://", "https://")):
599
+ self._print_error("Index URL must start with http:// or https://")
600
+ return self.EXIT_VALIDATION_ERROR
601
+
602
+ self.config.save_index(url)
603
+ self._print_success(f"Primary index set to: {url}")
604
+
605
+ return self.EXIT_SUCCESS
606
+
607
+ def _index_add(self, args: List[str]) -> int:
608
+ """Add extra index URL."""
609
+ self._print_warning(
610
+ "Adding extra index URLs is a security risk!\n"
611
+ "This can lead to dependency confusion attacks.\n"
612
+ "Consider using --index-url to switch indexes instead."
613
+ )
614
+
615
+ if not args:
616
+ self._print_error("Index URL required")
617
+ return self.EXIT_VALIDATION_ERROR
618
+
619
+ # For now, just print instructions
620
+ print("\nTo use extra index, run:")
621
+ print(f" praisonai install <pkg> --extra-index-url {args[0]} --allow-extra-index")
622
+
623
+ return self.EXIT_SUCCESS
624
+
625
+ def _index_remove(self, args: List[str]) -> int:
626
+ """Remove extra index URL."""
627
+ print("Extra index URLs are not persisted by default.")
628
+ print("To reset to PyPI, run:")
629
+ print(f" praisonai package index set {DEFAULT_INDEX_URL}")
630
+
631
+ return self.EXIT_SUCCESS