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.
- praisonai/__init__.py +54 -0
- praisonai/__main__.py +15 -0
- praisonai/acp/__init__.py +54 -0
- praisonai/acp/config.py +159 -0
- praisonai/acp/server.py +587 -0
- praisonai/acp/session.py +219 -0
- praisonai/adapters/__init__.py +50 -0
- praisonai/adapters/readers.py +395 -0
- praisonai/adapters/rerankers.py +315 -0
- praisonai/adapters/retrievers.py +394 -0
- praisonai/adapters/vector_stores.py +409 -0
- praisonai/agent_scheduler.py +337 -0
- praisonai/agents_generator.py +903 -0
- praisonai/api/call.py +292 -0
- praisonai/auto.py +1197 -0
- praisonai/capabilities/__init__.py +275 -0
- praisonai/capabilities/a2a.py +140 -0
- praisonai/capabilities/assistants.py +283 -0
- praisonai/capabilities/audio.py +320 -0
- praisonai/capabilities/batches.py +469 -0
- praisonai/capabilities/completions.py +336 -0
- praisonai/capabilities/container_files.py +155 -0
- praisonai/capabilities/containers.py +93 -0
- praisonai/capabilities/embeddings.py +158 -0
- praisonai/capabilities/files.py +467 -0
- praisonai/capabilities/fine_tuning.py +293 -0
- praisonai/capabilities/guardrails.py +182 -0
- praisonai/capabilities/images.py +330 -0
- praisonai/capabilities/mcp.py +190 -0
- praisonai/capabilities/messages.py +270 -0
- praisonai/capabilities/moderations.py +154 -0
- praisonai/capabilities/ocr.py +217 -0
- praisonai/capabilities/passthrough.py +204 -0
- praisonai/capabilities/rag.py +207 -0
- praisonai/capabilities/realtime.py +160 -0
- praisonai/capabilities/rerank.py +165 -0
- praisonai/capabilities/responses.py +266 -0
- praisonai/capabilities/search.py +109 -0
- praisonai/capabilities/skills.py +133 -0
- praisonai/capabilities/vector_store_files.py +334 -0
- praisonai/capabilities/vector_stores.py +304 -0
- praisonai/capabilities/videos.py +141 -0
- praisonai/chainlit_ui.py +304 -0
- praisonai/chat/__init__.py +106 -0
- praisonai/chat/app.py +125 -0
- praisonai/cli/__init__.py +26 -0
- praisonai/cli/app.py +213 -0
- praisonai/cli/commands/__init__.py +75 -0
- praisonai/cli/commands/acp.py +70 -0
- praisonai/cli/commands/completion.py +333 -0
- praisonai/cli/commands/config.py +166 -0
- praisonai/cli/commands/debug.py +142 -0
- praisonai/cli/commands/diag.py +55 -0
- praisonai/cli/commands/doctor.py +166 -0
- praisonai/cli/commands/environment.py +179 -0
- praisonai/cli/commands/lsp.py +112 -0
- praisonai/cli/commands/mcp.py +210 -0
- praisonai/cli/commands/profile.py +457 -0
- praisonai/cli/commands/run.py +228 -0
- praisonai/cli/commands/schedule.py +150 -0
- praisonai/cli/commands/serve.py +97 -0
- praisonai/cli/commands/session.py +212 -0
- praisonai/cli/commands/traces.py +145 -0
- praisonai/cli/commands/version.py +101 -0
- praisonai/cli/configuration/__init__.py +18 -0
- praisonai/cli/configuration/loader.py +353 -0
- praisonai/cli/configuration/paths.py +114 -0
- praisonai/cli/configuration/schema.py +164 -0
- praisonai/cli/features/__init__.py +268 -0
- praisonai/cli/features/acp.py +236 -0
- praisonai/cli/features/action_orchestrator.py +546 -0
- praisonai/cli/features/agent_scheduler.py +773 -0
- praisonai/cli/features/agent_tools.py +474 -0
- praisonai/cli/features/agents.py +375 -0
- praisonai/cli/features/at_mentions.py +471 -0
- praisonai/cli/features/auto_memory.py +182 -0
- praisonai/cli/features/autonomy_mode.py +490 -0
- praisonai/cli/features/background.py +356 -0
- praisonai/cli/features/base.py +168 -0
- praisonai/cli/features/capabilities.py +1326 -0
- praisonai/cli/features/checkpoints.py +338 -0
- praisonai/cli/features/code_intelligence.py +652 -0
- praisonai/cli/features/compaction.py +294 -0
- praisonai/cli/features/compare.py +534 -0
- praisonai/cli/features/cost_tracker.py +514 -0
- praisonai/cli/features/debug.py +810 -0
- praisonai/cli/features/deploy.py +517 -0
- praisonai/cli/features/diag.py +289 -0
- praisonai/cli/features/doctor/__init__.py +63 -0
- praisonai/cli/features/doctor/checks/__init__.py +24 -0
- praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
- praisonai/cli/features/doctor/checks/config_checks.py +366 -0
- praisonai/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai/cli/features/doctor/checks/env_checks.py +543 -0
- praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
- praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
- praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
- praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai/cli/features/doctor/engine.py +266 -0
- praisonai/cli/features/doctor/formatters.py +310 -0
- praisonai/cli/features/doctor/handler.py +397 -0
- praisonai/cli/features/doctor/models.py +264 -0
- praisonai/cli/features/doctor/registry.py +239 -0
- praisonai/cli/features/endpoints.py +1019 -0
- praisonai/cli/features/eval.py +560 -0
- praisonai/cli/features/external_agents.py +231 -0
- praisonai/cli/features/fast_context.py +410 -0
- praisonai/cli/features/flow_display.py +566 -0
- praisonai/cli/features/git_integration.py +651 -0
- praisonai/cli/features/guardrail.py +171 -0
- praisonai/cli/features/handoff.py +185 -0
- praisonai/cli/features/hooks.py +583 -0
- praisonai/cli/features/image.py +384 -0
- praisonai/cli/features/interactive_runtime.py +585 -0
- praisonai/cli/features/interactive_tools.py +380 -0
- praisonai/cli/features/interactive_tui.py +603 -0
- praisonai/cli/features/jobs.py +632 -0
- praisonai/cli/features/knowledge.py +531 -0
- praisonai/cli/features/lite.py +244 -0
- praisonai/cli/features/lsp_cli.py +225 -0
- praisonai/cli/features/mcp.py +169 -0
- praisonai/cli/features/message_queue.py +587 -0
- praisonai/cli/features/metrics.py +211 -0
- praisonai/cli/features/n8n.py +673 -0
- praisonai/cli/features/observability.py +293 -0
- praisonai/cli/features/ollama.py +361 -0
- praisonai/cli/features/output_style.py +273 -0
- praisonai/cli/features/package.py +631 -0
- praisonai/cli/features/performance.py +308 -0
- praisonai/cli/features/persistence.py +636 -0
- praisonai/cli/features/profile.py +226 -0
- praisonai/cli/features/profiler/__init__.py +81 -0
- praisonai/cli/features/profiler/core.py +558 -0
- praisonai/cli/features/profiler/optimizations.py +652 -0
- praisonai/cli/features/profiler/suite.py +386 -0
- praisonai/cli/features/profiling.py +350 -0
- praisonai/cli/features/queue/__init__.py +73 -0
- praisonai/cli/features/queue/manager.py +395 -0
- praisonai/cli/features/queue/models.py +286 -0
- praisonai/cli/features/queue/persistence.py +564 -0
- praisonai/cli/features/queue/scheduler.py +484 -0
- praisonai/cli/features/queue/worker.py +372 -0
- praisonai/cli/features/recipe.py +1723 -0
- praisonai/cli/features/recipes.py +449 -0
- praisonai/cli/features/registry.py +229 -0
- praisonai/cli/features/repo_map.py +860 -0
- praisonai/cli/features/router.py +466 -0
- praisonai/cli/features/sandbox_executor.py +515 -0
- praisonai/cli/features/serve.py +829 -0
- praisonai/cli/features/session.py +222 -0
- praisonai/cli/features/skills.py +856 -0
- praisonai/cli/features/slash_commands.py +650 -0
- praisonai/cli/features/telemetry.py +179 -0
- praisonai/cli/features/templates.py +1384 -0
- praisonai/cli/features/thinking.py +305 -0
- praisonai/cli/features/todo.py +334 -0
- praisonai/cli/features/tools.py +680 -0
- praisonai/cli/features/tui/__init__.py +83 -0
- praisonai/cli/features/tui/app.py +580 -0
- praisonai/cli/features/tui/cli.py +566 -0
- praisonai/cli/features/tui/debug.py +511 -0
- praisonai/cli/features/tui/events.py +99 -0
- praisonai/cli/features/tui/mock_provider.py +328 -0
- praisonai/cli/features/tui/orchestrator.py +652 -0
- praisonai/cli/features/tui/screens/__init__.py +50 -0
- praisonai/cli/features/tui/screens/main.py +245 -0
- praisonai/cli/features/tui/screens/queue.py +174 -0
- praisonai/cli/features/tui/screens/session.py +124 -0
- praisonai/cli/features/tui/screens/settings.py +148 -0
- praisonai/cli/features/tui/widgets/__init__.py +56 -0
- praisonai/cli/features/tui/widgets/chat.py +261 -0
- praisonai/cli/features/tui/widgets/composer.py +224 -0
- praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
- praisonai/cli/features/tui/widgets/status.py +167 -0
- praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
- praisonai/cli/features/workflow.py +720 -0
- praisonai/cli/legacy.py +236 -0
- praisonai/cli/main.py +5559 -0
- praisonai/cli/schedule_cli.py +54 -0
- praisonai/cli/state/__init__.py +31 -0
- praisonai/cli/state/identifiers.py +161 -0
- praisonai/cli/state/sessions.py +313 -0
- praisonai/code/__init__.py +93 -0
- praisonai/code/agent_tools.py +344 -0
- praisonai/code/diff/__init__.py +21 -0
- praisonai/code/diff/diff_strategy.py +432 -0
- praisonai/code/tools/__init__.py +27 -0
- praisonai/code/tools/apply_diff.py +221 -0
- praisonai/code/tools/execute_command.py +275 -0
- praisonai/code/tools/list_files.py +274 -0
- praisonai/code/tools/read_file.py +206 -0
- praisonai/code/tools/search_replace.py +248 -0
- praisonai/code/tools/write_file.py +217 -0
- praisonai/code/utils/__init__.py +46 -0
- praisonai/code/utils/file_utils.py +307 -0
- praisonai/code/utils/ignore_utils.py +308 -0
- praisonai/code/utils/text_utils.py +276 -0
- praisonai/db/__init__.py +64 -0
- praisonai/db/adapter.py +531 -0
- praisonai/deploy/__init__.py +62 -0
- praisonai/deploy/api.py +231 -0
- praisonai/deploy/docker.py +454 -0
- praisonai/deploy/doctor.py +367 -0
- praisonai/deploy/main.py +327 -0
- praisonai/deploy/models.py +179 -0
- praisonai/deploy/providers/__init__.py +33 -0
- praisonai/deploy/providers/aws.py +331 -0
- praisonai/deploy/providers/azure.py +358 -0
- praisonai/deploy/providers/base.py +101 -0
- praisonai/deploy/providers/gcp.py +314 -0
- praisonai/deploy/schema.py +208 -0
- praisonai/deploy.py +185 -0
- praisonai/endpoints/__init__.py +53 -0
- praisonai/endpoints/a2u_server.py +410 -0
- praisonai/endpoints/discovery.py +165 -0
- praisonai/endpoints/providers/__init__.py +28 -0
- praisonai/endpoints/providers/a2a.py +253 -0
- praisonai/endpoints/providers/a2u.py +208 -0
- praisonai/endpoints/providers/agents_api.py +171 -0
- praisonai/endpoints/providers/base.py +231 -0
- praisonai/endpoints/providers/mcp.py +263 -0
- praisonai/endpoints/providers/recipe.py +206 -0
- praisonai/endpoints/providers/tools_mcp.py +150 -0
- praisonai/endpoints/registry.py +131 -0
- praisonai/endpoints/server.py +161 -0
- praisonai/inbuilt_tools/__init__.py +24 -0
- praisonai/inbuilt_tools/autogen_tools.py +117 -0
- praisonai/inc/__init__.py +2 -0
- praisonai/inc/config.py +96 -0
- praisonai/inc/models.py +155 -0
- praisonai/integrations/__init__.py +56 -0
- praisonai/integrations/base.py +303 -0
- praisonai/integrations/claude_code.py +270 -0
- praisonai/integrations/codex_cli.py +255 -0
- praisonai/integrations/cursor_cli.py +195 -0
- praisonai/integrations/gemini_cli.py +222 -0
- praisonai/jobs/__init__.py +67 -0
- praisonai/jobs/executor.py +425 -0
- praisonai/jobs/models.py +230 -0
- praisonai/jobs/router.py +314 -0
- praisonai/jobs/server.py +186 -0
- praisonai/jobs/store.py +203 -0
- praisonai/llm/__init__.py +66 -0
- praisonai/llm/registry.py +382 -0
- praisonai/mcp_server/__init__.py +152 -0
- praisonai/mcp_server/adapters/__init__.py +74 -0
- praisonai/mcp_server/adapters/agents.py +128 -0
- praisonai/mcp_server/adapters/capabilities.py +168 -0
- praisonai/mcp_server/adapters/cli_tools.py +568 -0
- praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
- praisonai/mcp_server/adapters/knowledge.py +93 -0
- praisonai/mcp_server/adapters/memory.py +104 -0
- praisonai/mcp_server/adapters/prompts.py +306 -0
- praisonai/mcp_server/adapters/resources.py +124 -0
- praisonai/mcp_server/adapters/tools_bridge.py +280 -0
- praisonai/mcp_server/auth/__init__.py +48 -0
- praisonai/mcp_server/auth/api_key.py +291 -0
- praisonai/mcp_server/auth/oauth.py +460 -0
- praisonai/mcp_server/auth/oidc.py +289 -0
- praisonai/mcp_server/auth/scopes.py +260 -0
- praisonai/mcp_server/cli.py +852 -0
- praisonai/mcp_server/elicitation.py +445 -0
- praisonai/mcp_server/icons.py +302 -0
- praisonai/mcp_server/recipe_adapter.py +573 -0
- praisonai/mcp_server/recipe_cli.py +824 -0
- praisonai/mcp_server/registry.py +703 -0
- praisonai/mcp_server/sampling.py +422 -0
- praisonai/mcp_server/server.py +490 -0
- praisonai/mcp_server/tasks.py +443 -0
- praisonai/mcp_server/transports/__init__.py +18 -0
- praisonai/mcp_server/transports/http_stream.py +376 -0
- praisonai/mcp_server/transports/stdio.py +132 -0
- praisonai/persistence/__init__.py +84 -0
- praisonai/persistence/config.py +238 -0
- praisonai/persistence/conversation/__init__.py +25 -0
- praisonai/persistence/conversation/async_mysql.py +427 -0
- praisonai/persistence/conversation/async_postgres.py +410 -0
- praisonai/persistence/conversation/async_sqlite.py +371 -0
- praisonai/persistence/conversation/base.py +151 -0
- praisonai/persistence/conversation/json_store.py +250 -0
- praisonai/persistence/conversation/mysql.py +387 -0
- praisonai/persistence/conversation/postgres.py +401 -0
- praisonai/persistence/conversation/singlestore.py +240 -0
- praisonai/persistence/conversation/sqlite.py +341 -0
- praisonai/persistence/conversation/supabase.py +203 -0
- praisonai/persistence/conversation/surrealdb.py +287 -0
- praisonai/persistence/factory.py +301 -0
- praisonai/persistence/hooks/__init__.py +18 -0
- praisonai/persistence/hooks/agent_hooks.py +297 -0
- praisonai/persistence/knowledge/__init__.py +26 -0
- praisonai/persistence/knowledge/base.py +144 -0
- praisonai/persistence/knowledge/cassandra.py +232 -0
- praisonai/persistence/knowledge/chroma.py +295 -0
- praisonai/persistence/knowledge/clickhouse.py +242 -0
- praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
- praisonai/persistence/knowledge/couchbase.py +286 -0
- praisonai/persistence/knowledge/lancedb.py +216 -0
- praisonai/persistence/knowledge/langchain_adapter.py +291 -0
- praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
- praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
- praisonai/persistence/knowledge/milvus.py +277 -0
- praisonai/persistence/knowledge/mongodb_vector.py +306 -0
- praisonai/persistence/knowledge/pgvector.py +335 -0
- praisonai/persistence/knowledge/pinecone.py +253 -0
- praisonai/persistence/knowledge/qdrant.py +301 -0
- praisonai/persistence/knowledge/redis_vector.py +291 -0
- praisonai/persistence/knowledge/singlestore_vector.py +299 -0
- praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
- praisonai/persistence/knowledge/upstash_vector.py +266 -0
- praisonai/persistence/knowledge/weaviate.py +223 -0
- praisonai/persistence/migrations/__init__.py +10 -0
- praisonai/persistence/migrations/manager.py +251 -0
- praisonai/persistence/orchestrator.py +406 -0
- praisonai/persistence/state/__init__.py +21 -0
- praisonai/persistence/state/async_mongodb.py +200 -0
- praisonai/persistence/state/base.py +107 -0
- praisonai/persistence/state/dynamodb.py +226 -0
- praisonai/persistence/state/firestore.py +175 -0
- praisonai/persistence/state/gcs.py +155 -0
- praisonai/persistence/state/memory.py +245 -0
- praisonai/persistence/state/mongodb.py +158 -0
- praisonai/persistence/state/redis.py +190 -0
- praisonai/persistence/state/upstash.py +144 -0
- praisonai/persistence/tests/__init__.py +3 -0
- praisonai/persistence/tests/test_all_backends.py +633 -0
- praisonai/profiler.py +1214 -0
- praisonai/recipe/__init__.py +134 -0
- praisonai/recipe/bridge.py +278 -0
- praisonai/recipe/core.py +893 -0
- praisonai/recipe/exceptions.py +54 -0
- praisonai/recipe/history.py +402 -0
- praisonai/recipe/models.py +266 -0
- praisonai/recipe/operations.py +440 -0
- praisonai/recipe/policy.py +422 -0
- praisonai/recipe/registry.py +849 -0
- praisonai/recipe/runtime.py +214 -0
- praisonai/recipe/security.py +711 -0
- praisonai/recipe/serve.py +859 -0
- praisonai/recipe/server.py +613 -0
- praisonai/scheduler/__init__.py +45 -0
- praisonai/scheduler/agent_scheduler.py +552 -0
- praisonai/scheduler/base.py +124 -0
- praisonai/scheduler/daemon_manager.py +225 -0
- praisonai/scheduler/state_manager.py +155 -0
- praisonai/scheduler/yaml_loader.py +193 -0
- praisonai/scheduler.py +194 -0
- praisonai/setup/__init__.py +1 -0
- praisonai/setup/build.py +21 -0
- praisonai/setup/post_install.py +23 -0
- praisonai/setup/setup_conda_env.py +25 -0
- praisonai/setup.py +16 -0
- praisonai/templates/__init__.py +116 -0
- praisonai/templates/cache.py +364 -0
- praisonai/templates/dependency_checker.py +358 -0
- praisonai/templates/discovery.py +391 -0
- praisonai/templates/loader.py +564 -0
- praisonai/templates/registry.py +511 -0
- praisonai/templates/resolver.py +206 -0
- praisonai/templates/security.py +327 -0
- praisonai/templates/tool_override.py +498 -0
- praisonai/templates/tools_doctor.py +256 -0
- praisonai/test.py +105 -0
- praisonai/train.py +562 -0
- praisonai/train_vision.py +306 -0
- praisonai/ui/agents.py +824 -0
- praisonai/ui/callbacks.py +57 -0
- praisonai/ui/chainlit_compat.py +246 -0
- praisonai/ui/chat.py +532 -0
- praisonai/ui/code.py +717 -0
- praisonai/ui/colab.py +474 -0
- praisonai/ui/colab_chainlit.py +81 -0
- praisonai/ui/components/aicoder.py +284 -0
- praisonai/ui/context.py +283 -0
- praisonai/ui/database_config.py +56 -0
- praisonai/ui/db.py +294 -0
- praisonai/ui/realtime.py +488 -0
- praisonai/ui/realtimeclient/__init__.py +756 -0
- praisonai/ui/realtimeclient/tools.py +242 -0
- praisonai/ui/sql_alchemy.py +710 -0
- praisonai/upload_vision.py +140 -0
- praisonai/version.py +1 -0
- praisonai-3.0.0.dist-info/METADATA +3493 -0
- praisonai-3.0.0.dist-info/RECORD +393 -0
- praisonai-3.0.0.dist-info/WHEEL +5 -0
- praisonai-3.0.0.dist-info/entry_points.txt +4 -0
- praisonai-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read File Tool for PraisonAI Code.
|
|
3
|
+
|
|
4
|
+
Provides functionality to read file contents with optional line ranges
|
|
5
|
+
and line number annotations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Optional, List, Dict, Any
|
|
10
|
+
|
|
11
|
+
from ..utils.file_utils import (
|
|
12
|
+
add_line_numbers,
|
|
13
|
+
is_binary_file,
|
|
14
|
+
file_exists,
|
|
15
|
+
is_path_within_directory,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_file(
|
|
20
|
+
path: str,
|
|
21
|
+
start_line: Optional[int] = None,
|
|
22
|
+
end_line: Optional[int] = None,
|
|
23
|
+
add_line_nums: bool = True,
|
|
24
|
+
workspace: Optional[str] = None,
|
|
25
|
+
encoding: str = 'utf-8',
|
|
26
|
+
) -> Dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
Read file contents with optional line range and line numbers.
|
|
29
|
+
|
|
30
|
+
This tool reads the contents of a file, optionally limiting to specific
|
|
31
|
+
line ranges and adding line number annotations for AI context.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
path: Path to the file (absolute or relative to workspace)
|
|
35
|
+
start_line: First line to read (1-indexed, inclusive)
|
|
36
|
+
end_line: Last line to read (1-indexed, inclusive)
|
|
37
|
+
add_line_nums: Whether to add line numbers to output
|
|
38
|
+
workspace: Workspace root directory (for relative paths)
|
|
39
|
+
encoding: File encoding (default: utf-8)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dictionary with:
|
|
43
|
+
- success: bool
|
|
44
|
+
- content: str (file content, possibly with line numbers)
|
|
45
|
+
- total_lines: int (total lines in file)
|
|
46
|
+
- start_line: int (actual start line read)
|
|
47
|
+
- end_line: int (actual end line read)
|
|
48
|
+
- error: str (if success is False)
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> result = read_file("src/main.py", start_line=1, end_line=50)
|
|
52
|
+
>>> if result['success']:
|
|
53
|
+
... print(result['content'])
|
|
54
|
+
"""
|
|
55
|
+
# Resolve path
|
|
56
|
+
if workspace and not os.path.isabs(path):
|
|
57
|
+
abs_path = os.path.abspath(os.path.join(workspace, path))
|
|
58
|
+
else:
|
|
59
|
+
abs_path = os.path.abspath(path)
|
|
60
|
+
|
|
61
|
+
# Security check - ensure path is within workspace if specified
|
|
62
|
+
if workspace:
|
|
63
|
+
if not is_path_within_directory(abs_path, workspace):
|
|
64
|
+
return {
|
|
65
|
+
'success': False,
|
|
66
|
+
'error': f"Path '{path}' is outside the workspace",
|
|
67
|
+
'content': None,
|
|
68
|
+
'total_lines': 0,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Check if file exists
|
|
72
|
+
if not file_exists(abs_path):
|
|
73
|
+
return {
|
|
74
|
+
'success': False,
|
|
75
|
+
'error': f"File not found: {path}",
|
|
76
|
+
'content': None,
|
|
77
|
+
'total_lines': 0,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Check if binary
|
|
81
|
+
if is_binary_file(abs_path):
|
|
82
|
+
return {
|
|
83
|
+
'success': False,
|
|
84
|
+
'error': f"Cannot read binary file: {path}",
|
|
85
|
+
'content': None,
|
|
86
|
+
'total_lines': 0,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# Read file content
|
|
91
|
+
with open(abs_path, 'r', encoding=encoding, errors='replace') as f:
|
|
92
|
+
lines = f.readlines()
|
|
93
|
+
|
|
94
|
+
total_lines = len(lines)
|
|
95
|
+
|
|
96
|
+
# Determine line range
|
|
97
|
+
actual_start = start_line if start_line and start_line > 0 else 1
|
|
98
|
+
actual_end = end_line if end_line and end_line > 0 else total_lines
|
|
99
|
+
|
|
100
|
+
# Clamp to valid range
|
|
101
|
+
actual_start = max(1, min(actual_start, total_lines))
|
|
102
|
+
actual_end = max(actual_start, min(actual_end, total_lines))
|
|
103
|
+
|
|
104
|
+
# Extract lines (convert to 0-indexed)
|
|
105
|
+
selected_lines = lines[actual_start - 1:actual_end]
|
|
106
|
+
content = ''.join(selected_lines)
|
|
107
|
+
|
|
108
|
+
# Remove trailing newline if present
|
|
109
|
+
if content.endswith('\n'):
|
|
110
|
+
content = content[:-1]
|
|
111
|
+
|
|
112
|
+
# Add line numbers if requested
|
|
113
|
+
if add_line_nums:
|
|
114
|
+
content = add_line_numbers(content, actual_start)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
'success': True,
|
|
118
|
+
'content': content,
|
|
119
|
+
'total_lines': total_lines,
|
|
120
|
+
'start_line': actual_start,
|
|
121
|
+
'end_line': actual_end,
|
|
122
|
+
'path': path,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
except PermissionError:
|
|
126
|
+
return {
|
|
127
|
+
'success': False,
|
|
128
|
+
'error': f"Permission denied: {path}",
|
|
129
|
+
'content': None,
|
|
130
|
+
'total_lines': 0,
|
|
131
|
+
}
|
|
132
|
+
except UnicodeDecodeError as e:
|
|
133
|
+
return {
|
|
134
|
+
'success': False,
|
|
135
|
+
'error': f"Encoding error reading {path}: {e}",
|
|
136
|
+
'content': None,
|
|
137
|
+
'total_lines': 0,
|
|
138
|
+
}
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return {
|
|
141
|
+
'success': False,
|
|
142
|
+
'error': f"Error reading {path}: {str(e)}",
|
|
143
|
+
'content': None,
|
|
144
|
+
'total_lines': 0,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def read_multiple_files(
|
|
149
|
+
files: List[Dict[str, Any]],
|
|
150
|
+
workspace: Optional[str] = None,
|
|
151
|
+
add_line_nums: bool = True,
|
|
152
|
+
) -> Dict[str, Any]:
|
|
153
|
+
"""
|
|
154
|
+
Read multiple files in a single operation.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
files: List of file specifications, each with:
|
|
158
|
+
- path: str (required)
|
|
159
|
+
- start_line: int (optional)
|
|
160
|
+
- end_line: int (optional)
|
|
161
|
+
workspace: Workspace root directory
|
|
162
|
+
add_line_nums: Whether to add line numbers
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Dictionary with:
|
|
166
|
+
- success: bool (True if all files read successfully)
|
|
167
|
+
- results: List of individual file results
|
|
168
|
+
- errors: List of error messages
|
|
169
|
+
|
|
170
|
+
Example:
|
|
171
|
+
>>> files = [
|
|
172
|
+
... {'path': 'src/main.py', 'start_line': 1, 'end_line': 50},
|
|
173
|
+
... {'path': 'src/utils.py'},
|
|
174
|
+
... ]
|
|
175
|
+
>>> result = read_multiple_files(files, workspace='/project')
|
|
176
|
+
"""
|
|
177
|
+
results = []
|
|
178
|
+
errors = []
|
|
179
|
+
all_success = True
|
|
180
|
+
|
|
181
|
+
for file_spec in files:
|
|
182
|
+
path = file_spec.get('path')
|
|
183
|
+
if not path:
|
|
184
|
+
errors.append("Missing 'path' in file specification")
|
|
185
|
+
all_success = False
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
result = read_file(
|
|
189
|
+
path=path,
|
|
190
|
+
start_line=file_spec.get('start_line'),
|
|
191
|
+
end_line=file_spec.get('end_line'),
|
|
192
|
+
add_line_nums=add_line_nums,
|
|
193
|
+
workspace=workspace,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
results.append(result)
|
|
197
|
+
|
|
198
|
+
if not result['success']:
|
|
199
|
+
all_success = False
|
|
200
|
+
errors.append(result.get('error', f"Failed to read {path}"))
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
'success': all_success,
|
|
204
|
+
'results': results,
|
|
205
|
+
'errors': errors if errors else None,
|
|
206
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Search and Replace Tool for PraisonAI Code.
|
|
3
|
+
|
|
4
|
+
Provides functionality to perform multiple search/replace operations
|
|
5
|
+
on a file in a single operation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from typing import Optional, List, Dict, Any
|
|
11
|
+
|
|
12
|
+
from ..utils.file_utils import (
|
|
13
|
+
file_exists,
|
|
14
|
+
is_path_within_directory,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def search_replace(
|
|
19
|
+
path: str,
|
|
20
|
+
operations: List[Dict[str, Any]],
|
|
21
|
+
workspace: Optional[str] = None,
|
|
22
|
+
backup: bool = False,
|
|
23
|
+
encoding: str = 'utf-8',
|
|
24
|
+
) -> Dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Perform multiple search/replace operations on a file.
|
|
27
|
+
|
|
28
|
+
This tool allows precise modifications to file content by performing
|
|
29
|
+
multiple search and replace operations in sequence.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
path: Path to the file (absolute or relative to workspace)
|
|
33
|
+
operations: List of operations, each with:
|
|
34
|
+
- search: str (text to find)
|
|
35
|
+
- replace: str (text to replace with)
|
|
36
|
+
- is_regex: bool (optional, treat search as regex)
|
|
37
|
+
- count: int (optional, max replacements, -1 for all)
|
|
38
|
+
workspace: Workspace root directory
|
|
39
|
+
backup: Whether to create a backup before modifying
|
|
40
|
+
encoding: File encoding
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Dictionary with:
|
|
44
|
+
- success: bool
|
|
45
|
+
- path: str
|
|
46
|
+
- operations_applied: int
|
|
47
|
+
- total_replacements: int
|
|
48
|
+
- failed_operations: list
|
|
49
|
+
- error: str (if success is False)
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> operations = [
|
|
53
|
+
... {'search': 'old_name', 'replace': 'new_name'},
|
|
54
|
+
... {'search': r'def (\\w+)\\(', 'replace': r'def renamed_\\\\1(', 'is_regex': True},
|
|
55
|
+
... ]
|
|
56
|
+
>>> result = search_replace("src/main.py", operations)
|
|
57
|
+
"""
|
|
58
|
+
# Resolve path
|
|
59
|
+
if workspace and not os.path.isabs(path):
|
|
60
|
+
abs_path = os.path.abspath(os.path.join(workspace, path))
|
|
61
|
+
else:
|
|
62
|
+
abs_path = os.path.abspath(path)
|
|
63
|
+
|
|
64
|
+
# Security check
|
|
65
|
+
if workspace:
|
|
66
|
+
if not is_path_within_directory(abs_path, workspace):
|
|
67
|
+
return {
|
|
68
|
+
'success': False,
|
|
69
|
+
'error': f"Path '{path}' is outside the workspace",
|
|
70
|
+
'path': path,
|
|
71
|
+
'operations_applied': 0,
|
|
72
|
+
'total_replacements': 0,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Check if file exists
|
|
76
|
+
if not file_exists(abs_path):
|
|
77
|
+
return {
|
|
78
|
+
'success': False,
|
|
79
|
+
'error': f"File not found: {path}",
|
|
80
|
+
'path': path,
|
|
81
|
+
'operations_applied': 0,
|
|
82
|
+
'total_replacements': 0,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if not operations:
|
|
86
|
+
return {
|
|
87
|
+
'success': False,
|
|
88
|
+
'error': "No operations provided",
|
|
89
|
+
'path': path,
|
|
90
|
+
'operations_applied': 0,
|
|
91
|
+
'total_replacements': 0,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
# Read original content
|
|
96
|
+
with open(abs_path, 'r', encoding=encoding, errors='replace') as f:
|
|
97
|
+
content = f.read()
|
|
98
|
+
|
|
99
|
+
original_content = content
|
|
100
|
+
operations_applied = 0
|
|
101
|
+
total_replacements = 0
|
|
102
|
+
failed_operations = []
|
|
103
|
+
|
|
104
|
+
# Apply each operation
|
|
105
|
+
for i, op in enumerate(operations):
|
|
106
|
+
search = op.get('search')
|
|
107
|
+
replace = op.get('replace', '')
|
|
108
|
+
is_regex = op.get('is_regex', False)
|
|
109
|
+
count = op.get('count', -1) # -1 means replace all
|
|
110
|
+
|
|
111
|
+
if not search:
|
|
112
|
+
failed_operations.append({
|
|
113
|
+
'index': i,
|
|
114
|
+
'error': "Missing 'search' field",
|
|
115
|
+
})
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
if is_regex:
|
|
120
|
+
# Regex replacement
|
|
121
|
+
if count == -1:
|
|
122
|
+
new_content, num_subs = re.subn(search, replace, content)
|
|
123
|
+
else:
|
|
124
|
+
new_content, num_subs = re.subn(search, replace, content, count=count)
|
|
125
|
+
else:
|
|
126
|
+
# Literal replacement
|
|
127
|
+
if count == -1:
|
|
128
|
+
num_subs = content.count(search)
|
|
129
|
+
new_content = content.replace(search, replace)
|
|
130
|
+
else:
|
|
131
|
+
num_subs = min(content.count(search), count)
|
|
132
|
+
new_content = content
|
|
133
|
+
for _ in range(count):
|
|
134
|
+
if search in new_content:
|
|
135
|
+
new_content = new_content.replace(search, replace, 1)
|
|
136
|
+
else:
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
if num_subs > 0:
|
|
140
|
+
content = new_content
|
|
141
|
+
operations_applied += 1
|
|
142
|
+
total_replacements += num_subs
|
|
143
|
+
else:
|
|
144
|
+
failed_operations.append({
|
|
145
|
+
'index': i,
|
|
146
|
+
'search': search[:50],
|
|
147
|
+
'error': "No matches found",
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
except re.error as e:
|
|
151
|
+
failed_operations.append({
|
|
152
|
+
'index': i,
|
|
153
|
+
'search': search[:50],
|
|
154
|
+
'error': f"Invalid regex: {str(e)}",
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
# Check if any changes were made
|
|
158
|
+
if content == original_content:
|
|
159
|
+
return {
|
|
160
|
+
'success': False,
|
|
161
|
+
'error': "No changes were made - no matches found",
|
|
162
|
+
'path': path,
|
|
163
|
+
'operations_applied': 0,
|
|
164
|
+
'total_replacements': 0,
|
|
165
|
+
'failed_operations': failed_operations,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Create backup if requested
|
|
169
|
+
backup_path = None
|
|
170
|
+
if backup:
|
|
171
|
+
import time
|
|
172
|
+
timestamp = int(time.time())
|
|
173
|
+
backup_path = f"{abs_path}.backup.{timestamp}"
|
|
174
|
+
with open(backup_path, 'w', encoding=encoding) as f:
|
|
175
|
+
f.write(original_content)
|
|
176
|
+
|
|
177
|
+
# Write the modified content
|
|
178
|
+
with open(abs_path, 'w', encoding=encoding) as f:
|
|
179
|
+
f.write(content)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
'success': True,
|
|
183
|
+
'path': path,
|
|
184
|
+
'absolute_path': abs_path,
|
|
185
|
+
'operations_applied': operations_applied,
|
|
186
|
+
'total_replacements': total_replacements,
|
|
187
|
+
'failed_operations': failed_operations if failed_operations else None,
|
|
188
|
+
'backup_path': backup_path,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
except PermissionError:
|
|
192
|
+
return {
|
|
193
|
+
'success': False,
|
|
194
|
+
'error': f"Permission denied: {path}",
|
|
195
|
+
'path': path,
|
|
196
|
+
'operations_applied': 0,
|
|
197
|
+
'total_replacements': 0,
|
|
198
|
+
}
|
|
199
|
+
except Exception as e:
|
|
200
|
+
return {
|
|
201
|
+
'success': False,
|
|
202
|
+
'error': f"Error performing search/replace on {path}: {str(e)}",
|
|
203
|
+
'path': path,
|
|
204
|
+
'operations_applied': 0,
|
|
205
|
+
'total_replacements': 0,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def simple_replace(
|
|
210
|
+
path: str,
|
|
211
|
+
search: str,
|
|
212
|
+
replace: str,
|
|
213
|
+
workspace: Optional[str] = None,
|
|
214
|
+
is_regex: bool = False,
|
|
215
|
+
count: int = -1,
|
|
216
|
+
backup: bool = False,
|
|
217
|
+
encoding: str = 'utf-8',
|
|
218
|
+
) -> Dict[str, Any]:
|
|
219
|
+
"""
|
|
220
|
+
Perform a single search/replace operation on a file.
|
|
221
|
+
|
|
222
|
+
Convenience wrapper around search_replace for single operations.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
path: Path to the file
|
|
226
|
+
search: Text to find
|
|
227
|
+
replace: Text to replace with
|
|
228
|
+
workspace: Workspace root directory
|
|
229
|
+
is_regex: Whether search is a regex pattern
|
|
230
|
+
count: Max replacements (-1 for all)
|
|
231
|
+
backup: Whether to create a backup
|
|
232
|
+
encoding: File encoding
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Dictionary with success status and details
|
|
236
|
+
"""
|
|
237
|
+
return search_replace(
|
|
238
|
+
path=path,
|
|
239
|
+
operations=[{
|
|
240
|
+
'search': search,
|
|
241
|
+
'replace': replace,
|
|
242
|
+
'is_regex': is_regex,
|
|
243
|
+
'count': count,
|
|
244
|
+
}],
|
|
245
|
+
workspace=workspace,
|
|
246
|
+
backup=backup,
|
|
247
|
+
encoding=encoding,
|
|
248
|
+
)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Write File Tool for PraisonAI Code.
|
|
3
|
+
|
|
4
|
+
Provides functionality to create or overwrite files with content.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional, Dict, Any
|
|
9
|
+
|
|
10
|
+
from ..utils.file_utils import (
|
|
11
|
+
file_exists,
|
|
12
|
+
create_directories_for_file,
|
|
13
|
+
is_path_within_directory,
|
|
14
|
+
detect_line_ending,
|
|
15
|
+
normalize_line_endings,
|
|
16
|
+
)
|
|
17
|
+
from ..utils.text_utils import (
|
|
18
|
+
unescape_html_entities,
|
|
19
|
+
strip_markdown_code_fences,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def write_file(
|
|
24
|
+
path: str,
|
|
25
|
+
content: str,
|
|
26
|
+
workspace: Optional[str] = None,
|
|
27
|
+
create_directories: bool = True,
|
|
28
|
+
backup: bool = False,
|
|
29
|
+
strip_code_fences: bool = True,
|
|
30
|
+
encoding: str = 'utf-8',
|
|
31
|
+
) -> Dict[str, Any]:
|
|
32
|
+
"""
|
|
33
|
+
Write content to a file, creating it if it doesn't exist.
|
|
34
|
+
|
|
35
|
+
This tool writes content to a file, optionally creating parent
|
|
36
|
+
directories and handling common AI output artifacts like code fences.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
path: Path to the file (absolute or relative to workspace)
|
|
40
|
+
content: Content to write to the file
|
|
41
|
+
workspace: Workspace root directory (for relative paths)
|
|
42
|
+
create_directories: Whether to create parent directories
|
|
43
|
+
backup: Whether to create a backup of existing files
|
|
44
|
+
strip_code_fences: Whether to strip markdown code fences
|
|
45
|
+
encoding: File encoding (default: utf-8)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Dictionary with:
|
|
49
|
+
- success: bool
|
|
50
|
+
- path: str (the path written to)
|
|
51
|
+
- created: bool (True if file was created, False if overwritten)
|
|
52
|
+
- backup_path: str (if backup was created)
|
|
53
|
+
- error: str (if success is False)
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> result = write_file("src/new_file.py", "print('hello')")
|
|
57
|
+
>>> if result['success']:
|
|
58
|
+
... print(f"Wrote to {result['path']}")
|
|
59
|
+
"""
|
|
60
|
+
# Resolve path
|
|
61
|
+
if workspace and not os.path.isabs(path):
|
|
62
|
+
abs_path = os.path.abspath(os.path.join(workspace, path))
|
|
63
|
+
else:
|
|
64
|
+
abs_path = os.path.abspath(path)
|
|
65
|
+
|
|
66
|
+
# Security check - ensure path is within workspace if specified
|
|
67
|
+
if workspace:
|
|
68
|
+
if not is_path_within_directory(abs_path, workspace):
|
|
69
|
+
return {
|
|
70
|
+
'success': False,
|
|
71
|
+
'error': f"Path '{path}' is outside the workspace",
|
|
72
|
+
'path': path,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Process content
|
|
76
|
+
processed_content = content
|
|
77
|
+
|
|
78
|
+
# Strip markdown code fences if requested
|
|
79
|
+
if strip_code_fences:
|
|
80
|
+
processed_content = strip_markdown_code_fences(processed_content)
|
|
81
|
+
|
|
82
|
+
# Unescape HTML entities (common in AI output)
|
|
83
|
+
processed_content = unescape_html_entities(processed_content)
|
|
84
|
+
|
|
85
|
+
# Check if file exists
|
|
86
|
+
file_existed = file_exists(abs_path)
|
|
87
|
+
backup_path = None
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# Create backup if requested and file exists
|
|
91
|
+
if backup and file_existed:
|
|
92
|
+
import time
|
|
93
|
+
timestamp = int(time.time())
|
|
94
|
+
backup_path = f"{abs_path}.backup.{timestamp}"
|
|
95
|
+
with open(abs_path, 'r', encoding=encoding, errors='replace') as src:
|
|
96
|
+
with open(backup_path, 'w', encoding=encoding) as dst:
|
|
97
|
+
dst.write(src.read())
|
|
98
|
+
|
|
99
|
+
# Create parent directories if needed
|
|
100
|
+
if create_directories:
|
|
101
|
+
if not create_directories_for_file(abs_path):
|
|
102
|
+
return {
|
|
103
|
+
'success': False,
|
|
104
|
+
'error': f"Failed to create directories for {path}",
|
|
105
|
+
'path': path,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Preserve line endings if file exists
|
|
109
|
+
if file_existed:
|
|
110
|
+
try:
|
|
111
|
+
with open(abs_path, 'r', encoding=encoding, errors='replace') as f:
|
|
112
|
+
original_content = f.read()
|
|
113
|
+
original_line_ending = detect_line_ending(original_content)
|
|
114
|
+
processed_content = normalize_line_endings(processed_content, original_line_ending)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass # If we can't read, just use the content as-is
|
|
117
|
+
|
|
118
|
+
# Write the file
|
|
119
|
+
with open(abs_path, 'w', encoding=encoding) as f:
|
|
120
|
+
f.write(processed_content)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
'success': True,
|
|
124
|
+
'path': path,
|
|
125
|
+
'absolute_path': abs_path,
|
|
126
|
+
'created': not file_existed,
|
|
127
|
+
'backup_path': backup_path,
|
|
128
|
+
'bytes_written': len(processed_content.encode(encoding)),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
except PermissionError:
|
|
132
|
+
return {
|
|
133
|
+
'success': False,
|
|
134
|
+
'error': f"Permission denied: {path}",
|
|
135
|
+
'path': path,
|
|
136
|
+
}
|
|
137
|
+
except OSError as e:
|
|
138
|
+
return {
|
|
139
|
+
'success': False,
|
|
140
|
+
'error': f"OS error writing {path}: {str(e)}",
|
|
141
|
+
'path': path,
|
|
142
|
+
}
|
|
143
|
+
except Exception as e:
|
|
144
|
+
return {
|
|
145
|
+
'success': False,
|
|
146
|
+
'error': f"Error writing {path}: {str(e)}",
|
|
147
|
+
'path': path,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def append_to_file(
|
|
152
|
+
path: str,
|
|
153
|
+
content: str,
|
|
154
|
+
workspace: Optional[str] = None,
|
|
155
|
+
create_if_missing: bool = True,
|
|
156
|
+
encoding: str = 'utf-8',
|
|
157
|
+
) -> Dict[str, Any]:
|
|
158
|
+
"""
|
|
159
|
+
Append content to an existing file.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
path: Path to the file
|
|
163
|
+
content: Content to append
|
|
164
|
+
workspace: Workspace root directory
|
|
165
|
+
create_if_missing: Create file if it doesn't exist
|
|
166
|
+
encoding: File encoding
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Dictionary with success status and details
|
|
170
|
+
"""
|
|
171
|
+
# Resolve path
|
|
172
|
+
if workspace and not os.path.isabs(path):
|
|
173
|
+
abs_path = os.path.abspath(os.path.join(workspace, path))
|
|
174
|
+
else:
|
|
175
|
+
abs_path = os.path.abspath(path)
|
|
176
|
+
|
|
177
|
+
# Security check
|
|
178
|
+
if workspace:
|
|
179
|
+
if not is_path_within_directory(abs_path, workspace):
|
|
180
|
+
return {
|
|
181
|
+
'success': False,
|
|
182
|
+
'error': f"Path '{path}' is outside the workspace",
|
|
183
|
+
'path': path,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
file_existed = file_exists(abs_path)
|
|
187
|
+
|
|
188
|
+
if not file_existed and not create_if_missing:
|
|
189
|
+
return {
|
|
190
|
+
'success': False,
|
|
191
|
+
'error': f"File not found: {path}",
|
|
192
|
+
'path': path,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
# Create directories if needed
|
|
197
|
+
if not file_existed:
|
|
198
|
+
create_directories_for_file(abs_path)
|
|
199
|
+
|
|
200
|
+
# Append to file
|
|
201
|
+
with open(abs_path, 'a', encoding=encoding) as f:
|
|
202
|
+
f.write(content)
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
'success': True,
|
|
206
|
+
'path': path,
|
|
207
|
+
'absolute_path': abs_path,
|
|
208
|
+
'created': not file_existed,
|
|
209
|
+
'bytes_appended': len(content.encode(encoding)),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
return {
|
|
214
|
+
'success': False,
|
|
215
|
+
'error': f"Error appending to {path}: {str(e)}",
|
|
216
|
+
'path': path,
|
|
217
|
+
}
|