draft-board 0.1.0-beta.0
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.
- package/app/backend/.env.example +9 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
- package/app/backend/CURL_EXAMPLES.md +335 -0
- package/app/backend/ENV_SETUP.md +65 -0
- package/app/backend/alembic/env.py +71 -0
- package/app/backend/alembic/script.py.mako +28 -0
- package/app/backend/alembic/versions/001_initial_schema.py +104 -0
- package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
- package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
- package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
- package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
- package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
- package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
- package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
- package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
- package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
- package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
- package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
- package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
- package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
- package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
- package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
- package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
- package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
- package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
- package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
- package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
- package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
- package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
- package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
- package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
- package/app/backend/alembic/versions/add_job_variant.py +34 -0
- package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
- package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
- package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
- package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
- package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
- package/app/backend/alembic.ini +89 -0
- package/app/backend/app/__init__.py +3 -0
- package/app/backend/app/data_dir.py +85 -0
- package/app/backend/app/database.py +70 -0
- package/app/backend/app/database_sync.py +64 -0
- package/app/backend/app/dependencies/__init__.py +5 -0
- package/app/backend/app/dependencies/auth.py +80 -0
- package/app/backend/app/dependencies.py +43 -0
- package/app/backend/app/exceptions.py +178 -0
- package/app/backend/app/executors/__init__.py +1 -0
- package/app/backend/app/executors/adapters/__init__.py +1 -0
- package/app/backend/app/executors/adapters/aider.py +152 -0
- package/app/backend/app/executors/adapters/amazon_q.py +103 -0
- package/app/backend/app/executors/adapters/amp.py +123 -0
- package/app/backend/app/executors/adapters/claude.py +177 -0
- package/app/backend/app/executors/adapters/cline.py +127 -0
- package/app/backend/app/executors/adapters/codex.py +167 -0
- package/app/backend/app/executors/adapters/copilot.py +202 -0
- package/app/backend/app/executors/adapters/cursor.py +87 -0
- package/app/backend/app/executors/adapters/droid.py +123 -0
- package/app/backend/app/executors/adapters/gemini.py +132 -0
- package/app/backend/app/executors/adapters/goose.py +131 -0
- package/app/backend/app/executors/adapters/opencode.py +123 -0
- package/app/backend/app/executors/adapters/qwen.py +123 -0
- package/app/backend/app/executors/plugins/__init__.py +1 -0
- package/app/backend/app/executors/registry.py +202 -0
- package/app/backend/app/executors/spec.py +226 -0
- package/app/backend/app/main.py +486 -0
- package/app/backend/app/middleware/__init__.py +13 -0
- package/app/backend/app/middleware/idempotency.py +426 -0
- package/app/backend/app/middleware/rate_limit.py +312 -0
- package/app/backend/app/middleware/security_headers.py +43 -0
- package/app/backend/app/middleware/timeout.py +37 -0
- package/app/backend/app/models/__init__.py +56 -0
- package/app/backend/app/models/agent_conversation_history.py +56 -0
- package/app/backend/app/models/agent_session.py +127 -0
- package/app/backend/app/models/analysis_cache.py +49 -0
- package/app/backend/app/models/base.py +9 -0
- package/app/backend/app/models/board.py +79 -0
- package/app/backend/app/models/board_repo.py +68 -0
- package/app/backend/app/models/cost_budget.py +42 -0
- package/app/backend/app/models/enums.py +40 -0
- package/app/backend/app/models/evidence.py +132 -0
- package/app/backend/app/models/goal.py +102 -0
- package/app/backend/app/models/idempotency_entry.py +30 -0
- package/app/backend/app/models/job.py +163 -0
- package/app/backend/app/models/job_queue.py +39 -0
- package/app/backend/app/models/kv_store.py +28 -0
- package/app/backend/app/models/merge_checklist.py +87 -0
- package/app/backend/app/models/normalized_log.py +100 -0
- package/app/backend/app/models/planner_lock.py +43 -0
- package/app/backend/app/models/rate_limit_entry.py +25 -0
- package/app/backend/app/models/repo.py +66 -0
- package/app/backend/app/models/review_comment.py +91 -0
- package/app/backend/app/models/review_summary.py +69 -0
- package/app/backend/app/models/revision.py +130 -0
- package/app/backend/app/models/ticket.py +223 -0
- package/app/backend/app/models/ticket_event.py +83 -0
- package/app/backend/app/models/user.py +47 -0
- package/app/backend/app/models/workspace.py +71 -0
- package/app/backend/app/redis_client.py +119 -0
- package/app/backend/app/routers/__init__.py +29 -0
- package/app/backend/app/routers/agents.py +296 -0
- package/app/backend/app/routers/auth.py +94 -0
- package/app/backend/app/routers/board.py +885 -0
- package/app/backend/app/routers/dashboard.py +351 -0
- package/app/backend/app/routers/debug.py +528 -0
- package/app/backend/app/routers/evidence.py +96 -0
- package/app/backend/app/routers/executors.py +324 -0
- package/app/backend/app/routers/goals.py +574 -0
- package/app/backend/app/routers/jobs.py +448 -0
- package/app/backend/app/routers/maintenance.py +172 -0
- package/app/backend/app/routers/merge.py +360 -0
- package/app/backend/app/routers/planner.py +537 -0
- package/app/backend/app/routers/pull_requests.py +382 -0
- package/app/backend/app/routers/repos.py +263 -0
- package/app/backend/app/routers/revisions.py +939 -0
- package/app/backend/app/routers/settings.py +267 -0
- package/app/backend/app/routers/tickets.py +2003 -0
- package/app/backend/app/routers/webhooks.py +143 -0
- package/app/backend/app/routers/websocket.py +249 -0
- package/app/backend/app/schemas/__init__.py +109 -0
- package/app/backend/app/schemas/board.py +87 -0
- package/app/backend/app/schemas/common.py +33 -0
- package/app/backend/app/schemas/evidence.py +87 -0
- package/app/backend/app/schemas/goal.py +90 -0
- package/app/backend/app/schemas/job.py +97 -0
- package/app/backend/app/schemas/merge.py +139 -0
- package/app/backend/app/schemas/planner.py +500 -0
- package/app/backend/app/schemas/repo.py +187 -0
- package/app/backend/app/schemas/review.py +137 -0
- package/app/backend/app/schemas/revision.py +114 -0
- package/app/backend/app/schemas/ticket.py +238 -0
- package/app/backend/app/schemas/ticket_event.py +72 -0
- package/app/backend/app/schemas/workspace.py +19 -0
- package/app/backend/app/services/__init__.py +31 -0
- package/app/backend/app/services/agent_memory_service.py +223 -0
- package/app/backend/app/services/agent_registry.py +346 -0
- package/app/backend/app/services/agent_session_manager.py +318 -0
- package/app/backend/app/services/agent_session_service.py +219 -0
- package/app/backend/app/services/agent_tools.py +379 -0
- package/app/backend/app/services/auth_service.py +98 -0
- package/app/backend/app/services/autonomy_service.py +380 -0
- package/app/backend/app/services/board_repo_service.py +201 -0
- package/app/backend/app/services/board_service.py +326 -0
- package/app/backend/app/services/cleanup_service.py +1085 -0
- package/app/backend/app/services/config_service.py +908 -0
- package/app/backend/app/services/context_gatherer.py +557 -0
- package/app/backend/app/services/cost_tracking_service.py +293 -0
- package/app/backend/app/services/cursor_log_normalizer.py +536 -0
- package/app/backend/app/services/delivery_pipeline.py +440 -0
- package/app/backend/app/services/executor_service.py +634 -0
- package/app/backend/app/services/git_host/__init__.py +11 -0
- package/app/backend/app/services/git_host/factory.py +87 -0
- package/app/backend/app/services/git_host/github.py +270 -0
- package/app/backend/app/services/git_host/gitlab.py +194 -0
- package/app/backend/app/services/git_host/protocol.py +75 -0
- package/app/backend/app/services/git_merge_simple.py +346 -0
- package/app/backend/app/services/git_ops.py +384 -0
- package/app/backend/app/services/github_service.py +233 -0
- package/app/backend/app/services/goal_service.py +113 -0
- package/app/backend/app/services/job_service.py +423 -0
- package/app/backend/app/services/job_watchdog_service.py +424 -0
- package/app/backend/app/services/langchain_adapter.py +122 -0
- package/app/backend/app/services/llm_provider_clients.py +351 -0
- package/app/backend/app/services/llm_service.py +285 -0
- package/app/backend/app/services/log_normalizer.py +342 -0
- package/app/backend/app/services/log_stream_service.py +276 -0
- package/app/backend/app/services/merge_checklist_service.py +264 -0
- package/app/backend/app/services/merge_service.py +784 -0
- package/app/backend/app/services/orchestrator_log.py +84 -0
- package/app/backend/app/services/planner_service.py +1662 -0
- package/app/backend/app/services/planner_tick_sync.py +1040 -0
- package/app/backend/app/services/queued_message_service.py +156 -0
- package/app/backend/app/services/reliability_wrapper.py +389 -0
- package/app/backend/app/services/repo_discovery_service.py +318 -0
- package/app/backend/app/services/review_service.py +334 -0
- package/app/backend/app/services/revision_service.py +389 -0
- package/app/backend/app/services/safe_autopilot.py +510 -0
- package/app/backend/app/services/sqlite_worker.py +372 -0
- package/app/backend/app/services/task_dispatch.py +135 -0
- package/app/backend/app/services/ticket_generation_service.py +1781 -0
- package/app/backend/app/services/ticket_service.py +486 -0
- package/app/backend/app/services/udar_planner_service.py +1007 -0
- package/app/backend/app/services/webhook_service.py +126 -0
- package/app/backend/app/services/workspace_service.py +465 -0
- package/app/backend/app/services/worktree_file_service.py +92 -0
- package/app/backend/app/services/worktree_validator.py +213 -0
- package/app/backend/app/sqlite_kv.py +278 -0
- package/app/backend/app/state_machine.py +128 -0
- package/app/backend/app/templates/__init__.py +5 -0
- package/app/backend/app/templates/registry.py +243 -0
- package/app/backend/app/utils/__init__.py +5 -0
- package/app/backend/app/utils/artifact_reader.py +87 -0
- package/app/backend/app/utils/circuit_breaker.py +229 -0
- package/app/backend/app/utils/db_retry.py +136 -0
- package/app/backend/app/utils/ignored_fields.py +123 -0
- package/app/backend/app/utils/validators.py +54 -0
- package/app/backend/app/websocket/__init__.py +5 -0
- package/app/backend/app/websocket/manager.py +179 -0
- package/app/backend/app/websocket/state_tracker.py +113 -0
- package/app/backend/app/worker.py +3190 -0
- package/app/backend/calculator_tickets.json +40 -0
- package/app/backend/canary_tests.sh +591 -0
- package/app/backend/celerybeat-schedule +0 -0
- package/app/backend/celerybeat-schedule-shm +0 -0
- package/app/backend/celerybeat-schedule-wal +0 -0
- package/app/backend/logs/.gitkeep +3 -0
- package/app/backend/multiplication_division_implementation_tickets.json +55 -0
- package/app/backend/multiplication_division_tickets.json +42 -0
- package/app/backend/pyproject.toml +45 -0
- package/app/backend/requirements-dev.txt +8 -0
- package/app/backend/requirements.txt +20 -0
- package/app/backend/run.sh +30 -0
- package/app/backend/run_with_logs.sh +10 -0
- package/app/backend/scientific_calculator_tickets.json +40 -0
- package/app/backend/scripts/extract_openapi.py +21 -0
- package/app/backend/scripts/seed_demo.py +187 -0
- package/app/backend/setup_demo_review.py +302 -0
- package/app/backend/test_actual_parse.py +41 -0
- package/app/backend/test_agent_streaming.py +61 -0
- package/app/backend/test_parse.py +51 -0
- package/app/backend/test_streaming.py +51 -0
- package/app/backend/test_subprocess_streaming.py +50 -0
- package/app/backend/tests/__init__.py +1 -0
- package/app/backend/tests/conftest.py +46 -0
- package/app/backend/tests/test_auth.py +341 -0
- package/app/backend/tests/test_autonomy_service.py +391 -0
- package/app/backend/tests/test_cleanup_service_safety.py +417 -0
- package/app/backend/tests/test_middleware.py +279 -0
- package/app/backend/tests/test_planner_providers.py +290 -0
- package/app/backend/tests/test_planner_unblock.py +183 -0
- package/app/backend/tests/test_revision_invariants.py +618 -0
- package/app/backend/tests/test_sqlite_kv.py +290 -0
- package/app/backend/tests/test_sqlite_worker.py +353 -0
- package/app/backend/tests/test_task_dispatch.py +100 -0
- package/app/backend/tests/test_ticket_validation.py +304 -0
- package/app/backend/tests/test_udar_agent.py +693 -0
- package/app/backend/tests/test_webhook_service.py +184 -0
- package/app/backend/tickets_output.json +59 -0
- package/app/backend/user_management_tickets.json +50 -0
- package/app/backend/uvicorn.log +0 -0
- package/app/draft.yaml +313 -0
- package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
- package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
- package/app/frontend/dist/index.html +14 -0
- package/app/frontend/dist/vite.svg +1 -0
- package/app/frontend/package.json +101 -0
- package/bin/cli.js +527 -0
- package/package.json +37 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Cursor AI IDE adapter."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
from app.executors.registry import ExecutorRegistry
|
|
8
|
+
from app.executors.spec import (
|
|
9
|
+
ExecutionRequest,
|
|
10
|
+
ExecutionResult,
|
|
11
|
+
ExecutorAdapter,
|
|
12
|
+
ExecutorCapability,
|
|
13
|
+
ExecutorInvocationError,
|
|
14
|
+
ExecutorMetadata,
|
|
15
|
+
ExecutorNotFoundError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@ExecutorRegistry.register("cursor")
|
|
20
|
+
class CursorAdapter(ExecutorAdapter):
|
|
21
|
+
"""Cursor AI IDE adapter."""
|
|
22
|
+
|
|
23
|
+
def get_metadata(self) -> ExecutorMetadata:
|
|
24
|
+
return ExecutorMetadata(
|
|
25
|
+
name="cursor",
|
|
26
|
+
display_name="Cursor",
|
|
27
|
+
version="1.0.0",
|
|
28
|
+
capabilities=[
|
|
29
|
+
ExecutorCapability.INTERACTIVE, # Opens IDE
|
|
30
|
+
ExecutorCapability.MCP_SERVERS,
|
|
31
|
+
],
|
|
32
|
+
config_schema={
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"auto_apply": {
|
|
36
|
+
"type": "boolean",
|
|
37
|
+
"default": False,
|
|
38
|
+
"description": "Auto-apply suggestions without confirmation",
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
documentation_url="https://cursor.sh/",
|
|
43
|
+
author="Cursor",
|
|
44
|
+
license="Proprietary",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async def is_available(self) -> bool:
|
|
48
|
+
"""Check if cursor is installed."""
|
|
49
|
+
return shutil.which("cursor") is not None
|
|
50
|
+
|
|
51
|
+
async def execute(self, request: ExecutionRequest) -> ExecutionResult:
|
|
52
|
+
"""Execute using Cursor.
|
|
53
|
+
|
|
54
|
+
Note: Cursor is primarily interactive, so this opens the IDE
|
|
55
|
+
and requires human interaction to complete the task.
|
|
56
|
+
"""
|
|
57
|
+
if not await self.is_available():
|
|
58
|
+
raise ExecutorNotFoundError(
|
|
59
|
+
"Cursor not found. Install from https://cursor.sh/"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Build command - opens Cursor in the working directory
|
|
63
|
+
cmd = ["cursor", request.working_directory]
|
|
64
|
+
|
|
65
|
+
# Note: Cursor doesn't have a headless mode for autonomous execution
|
|
66
|
+
# This will open the IDE and the human must complete the work
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
await asyncio.create_subprocess_exec(
|
|
70
|
+
*cmd,
|
|
71
|
+
stdout=asyncio.subprocess.PIPE,
|
|
72
|
+
stderr=asyncio.subprocess.PIPE,
|
|
73
|
+
env={**os.environ, **request.environment},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# For Cursor, we just launch it and return immediately
|
|
77
|
+
# The actual work happens interactively
|
|
78
|
+
|
|
79
|
+
return ExecutionResult(
|
|
80
|
+
exit_code=0,
|
|
81
|
+
stdout=f"Opened Cursor in {request.working_directory}\\nPrompt: {request.prompt}",
|
|
82
|
+
stderr="",
|
|
83
|
+
metadata={"interactive": True, "requires_human": True},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
raise ExecutorInvocationError(f"Cursor execution failed: {str(e)}")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Droid CLI adapter."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
|
|
8
|
+
from app.executors.registry import ExecutorRegistry
|
|
9
|
+
from app.executors.spec import (
|
|
10
|
+
ExecutionRequest,
|
|
11
|
+
ExecutionResult,
|
|
12
|
+
ExecutorAdapter,
|
|
13
|
+
ExecutorCapability,
|
|
14
|
+
ExecutorInvocationError,
|
|
15
|
+
ExecutorMetadata,
|
|
16
|
+
ExecutorNotFoundError,
|
|
17
|
+
ExecutorTimeoutError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@ExecutorRegistry.register("droid")
|
|
22
|
+
class DroidAdapter(ExecutorAdapter):
|
|
23
|
+
"""Droid CLI adapter for automated code changes."""
|
|
24
|
+
|
|
25
|
+
def get_metadata(self) -> ExecutorMetadata:
|
|
26
|
+
return ExecutorMetadata(
|
|
27
|
+
name="droid",
|
|
28
|
+
display_name="Droid CLI",
|
|
29
|
+
version="1.0.0",
|
|
30
|
+
capabilities=[
|
|
31
|
+
ExecutorCapability.STREAMING_OUTPUT,
|
|
32
|
+
ExecutorCapability.YOLO_MODE,
|
|
33
|
+
],
|
|
34
|
+
config_schema={
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"model": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"default": "default",
|
|
40
|
+
"description": "Model to use for Droid",
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
documentation_url="https://github.com/anthropics/droid",
|
|
45
|
+
author="Anthropic",
|
|
46
|
+
license="MIT",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def is_available(self) -> bool:
|
|
50
|
+
"""Check if droid CLI is installed."""
|
|
51
|
+
return shutil.which("droid") is not None
|
|
52
|
+
|
|
53
|
+
async def execute(self, request: ExecutionRequest) -> ExecutionResult:
|
|
54
|
+
"""Execute using Droid CLI."""
|
|
55
|
+
if not await self.is_available():
|
|
56
|
+
raise ExecutorNotFoundError(
|
|
57
|
+
"Droid CLI not found. Install from https://github.com/anthropics/droid"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
cmd = ["droid", "--print"]
|
|
61
|
+
|
|
62
|
+
if request.yolo_mode:
|
|
63
|
+
cmd.append("--dangerously-skip-permissions")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
process = await asyncio.create_subprocess_exec(
|
|
67
|
+
*cmd,
|
|
68
|
+
cwd=request.working_directory,
|
|
69
|
+
stdin=asyncio.subprocess.PIPE,
|
|
70
|
+
stdout=asyncio.subprocess.PIPE,
|
|
71
|
+
stderr=asyncio.subprocess.PIPE,
|
|
72
|
+
env={**os.environ, **request.environment},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
stdout, stderr = await asyncio.wait_for(
|
|
76
|
+
process.communicate(input=request.prompt.encode("utf-8")),
|
|
77
|
+
timeout=request.timeout_seconds,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return ExecutionResult(
|
|
81
|
+
exit_code=process.returncode,
|
|
82
|
+
stdout=stdout.decode("utf-8", errors="replace"),
|
|
83
|
+
stderr=stderr.decode("utf-8", errors="replace"),
|
|
84
|
+
duration_seconds=0.0,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
except TimeoutError:
|
|
88
|
+
process.kill()
|
|
89
|
+
raise ExecutorTimeoutError(
|
|
90
|
+
f"Droid execution timed out after {request.timeout_seconds}s"
|
|
91
|
+
) from None
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise ExecutorInvocationError(f"Droid execution failed: {e!s}") from e
|
|
94
|
+
|
|
95
|
+
async def stream_output(self, request: ExecutionRequest) -> AsyncIterator[str]:
|
|
96
|
+
"""Stream output in real-time."""
|
|
97
|
+
if not await self.is_available():
|
|
98
|
+
raise ExecutorNotFoundError("Droid CLI not found")
|
|
99
|
+
|
|
100
|
+
cmd = ["droid", "--print"]
|
|
101
|
+
if request.yolo_mode:
|
|
102
|
+
cmd.append("--dangerously-skip-permissions")
|
|
103
|
+
|
|
104
|
+
process = await asyncio.create_subprocess_exec(
|
|
105
|
+
*cmd,
|
|
106
|
+
cwd=request.working_directory,
|
|
107
|
+
stdin=asyncio.subprocess.PIPE,
|
|
108
|
+
stdout=asyncio.subprocess.PIPE,
|
|
109
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
110
|
+
env={**os.environ, **request.environment},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
process.stdin.write(request.prompt.encode("utf-8"))
|
|
114
|
+
await process.stdin.drain()
|
|
115
|
+
process.stdin.close()
|
|
116
|
+
|
|
117
|
+
while True:
|
|
118
|
+
line = await process.stdout.readline()
|
|
119
|
+
if not line:
|
|
120
|
+
break
|
|
121
|
+
yield line.decode("utf-8", errors="replace")
|
|
122
|
+
|
|
123
|
+
await process.wait()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Google Gemini CLI adapter."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
|
|
8
|
+
from app.executors.registry import ExecutorRegistry
|
|
9
|
+
from app.executors.spec import (
|
|
10
|
+
ExecutionRequest,
|
|
11
|
+
ExecutionResult,
|
|
12
|
+
ExecutorAdapter,
|
|
13
|
+
ExecutorCapability,
|
|
14
|
+
ExecutorInvocationError,
|
|
15
|
+
ExecutorMetadata,
|
|
16
|
+
ExecutorNotFoundError,
|
|
17
|
+
ExecutorTimeoutError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@ExecutorRegistry.register("gemini")
|
|
22
|
+
class GeminiAdapter(ExecutorAdapter):
|
|
23
|
+
"""Google Gemini CLI adapter."""
|
|
24
|
+
|
|
25
|
+
def get_metadata(self) -> ExecutorMetadata:
|
|
26
|
+
return ExecutorMetadata(
|
|
27
|
+
name="gemini",
|
|
28
|
+
display_name="Google Gemini CLI",
|
|
29
|
+
version="1.0.0",
|
|
30
|
+
capabilities=[
|
|
31
|
+
ExecutorCapability.STREAMING_OUTPUT,
|
|
32
|
+
ExecutorCapability.YOLO_MODE,
|
|
33
|
+
],
|
|
34
|
+
config_schema={
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"model": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"default": "gemini-2.0-flash",
|
|
40
|
+
"enum": ["gemini-2.0-flash", "gemini-pro", "gemini-ultra"],
|
|
41
|
+
"description": "Gemini model to use",
|
|
42
|
+
},
|
|
43
|
+
"sandbox": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"enum": ["docker", "local"],
|
|
46
|
+
"default": "docker",
|
|
47
|
+
"description": "Execution sandbox environment",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
documentation_url="https://github.com/google/gemini-cli",
|
|
52
|
+
author="Google",
|
|
53
|
+
license="Apache-2.0",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def is_available(self) -> bool:
|
|
57
|
+
"""Check if gemini CLI is installed."""
|
|
58
|
+
return shutil.which("gemini") is not None
|
|
59
|
+
|
|
60
|
+
async def execute(self, request: ExecutionRequest) -> ExecutionResult:
|
|
61
|
+
"""Execute using Gemini CLI."""
|
|
62
|
+
if not await self.is_available():
|
|
63
|
+
raise ExecutorNotFoundError(
|
|
64
|
+
"Gemini CLI not found. Install from https://github.com/google/gemini-cli"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Build command
|
|
68
|
+
cmd = ["gemini", "--print"]
|
|
69
|
+
|
|
70
|
+
if request.yolo_mode:
|
|
71
|
+
cmd.append("--yolo")
|
|
72
|
+
|
|
73
|
+
# Add model if specified
|
|
74
|
+
model = request.config.get("model", "gemini-2.0-flash")
|
|
75
|
+
cmd.extend(["--model", model])
|
|
76
|
+
|
|
77
|
+
# Add the prompt
|
|
78
|
+
cmd.extend(["--prompt", request.prompt])
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
process = await asyncio.create_subprocess_exec(
|
|
82
|
+
*cmd,
|
|
83
|
+
cwd=request.working_directory,
|
|
84
|
+
stdout=asyncio.subprocess.PIPE,
|
|
85
|
+
stderr=asyncio.subprocess.PIPE,
|
|
86
|
+
env={**os.environ, **request.environment},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
stdout, stderr = await asyncio.wait_for(
|
|
90
|
+
process.communicate(), timeout=request.timeout_seconds
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return ExecutionResult(
|
|
94
|
+
exit_code=process.returncode,
|
|
95
|
+
stdout=stdout.decode("utf-8", errors="replace"),
|
|
96
|
+
stderr=stderr.decode("utf-8", errors="replace"),
|
|
97
|
+
duration_seconds=0.0,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
except TimeoutError:
|
|
101
|
+
process.kill()
|
|
102
|
+
raise ExecutorTimeoutError(
|
|
103
|
+
f"Gemini execution timed out after {request.timeout_seconds}s"
|
|
104
|
+
)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
raise ExecutorInvocationError(f"Gemini execution failed: {str(e)}")
|
|
107
|
+
|
|
108
|
+
async def stream_output(self, request: ExecutionRequest) -> AsyncIterator[str]:
|
|
109
|
+
"""Stream output in real-time."""
|
|
110
|
+
if not await self.is_available():
|
|
111
|
+
raise ExecutorNotFoundError("Gemini CLI not found")
|
|
112
|
+
|
|
113
|
+
cmd = ["gemini", "--print"]
|
|
114
|
+
if request.yolo_mode:
|
|
115
|
+
cmd.append("--yolo")
|
|
116
|
+
cmd.extend(["--prompt", request.prompt])
|
|
117
|
+
|
|
118
|
+
process = await asyncio.create_subprocess_exec(
|
|
119
|
+
*cmd,
|
|
120
|
+
cwd=request.working_directory,
|
|
121
|
+
stdout=asyncio.subprocess.PIPE,
|
|
122
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
123
|
+
env={**os.environ, **request.environment},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
while True:
|
|
127
|
+
line = await process.stdout.readline()
|
|
128
|
+
if not line:
|
|
129
|
+
break
|
|
130
|
+
yield line.decode("utf-8", errors="replace")
|
|
131
|
+
|
|
132
|
+
await process.wait()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Goose AI assistant adapter."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
|
|
8
|
+
from app.executors.registry import ExecutorRegistry
|
|
9
|
+
from app.executors.spec import (
|
|
10
|
+
ExecutionRequest,
|
|
11
|
+
ExecutionResult,
|
|
12
|
+
ExecutorAdapter,
|
|
13
|
+
ExecutorCapability,
|
|
14
|
+
ExecutorInvocationError,
|
|
15
|
+
ExecutorMetadata,
|
|
16
|
+
ExecutorNotFoundError,
|
|
17
|
+
ExecutorTimeoutError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@ExecutorRegistry.register("goose")
|
|
22
|
+
class GooseAdapter(ExecutorAdapter):
|
|
23
|
+
"""Goose AI assistant adapter."""
|
|
24
|
+
|
|
25
|
+
def get_metadata(self) -> ExecutorMetadata:
|
|
26
|
+
return ExecutorMetadata(
|
|
27
|
+
name="goose",
|
|
28
|
+
display_name="Goose",
|
|
29
|
+
version="1.0.0",
|
|
30
|
+
capabilities=[
|
|
31
|
+
ExecutorCapability.STREAMING_OUTPUT,
|
|
32
|
+
ExecutorCapability.SESSION_RESUME,
|
|
33
|
+
],
|
|
34
|
+
config_schema={
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"model": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"default": "gpt-4",
|
|
40
|
+
"description": "LLM model to use",
|
|
41
|
+
},
|
|
42
|
+
"profile": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "Goose profile to use",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
documentation_url="https://github.com/square/goose",
|
|
49
|
+
author="Square",
|
|
50
|
+
license="Apache-2.0",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async def is_available(self) -> bool:
|
|
54
|
+
"""Check if goose is installed."""
|
|
55
|
+
return shutil.which("goose") is not None
|
|
56
|
+
|
|
57
|
+
async def execute(self, request: ExecutionRequest) -> ExecutionResult:
|
|
58
|
+
"""Execute using Goose."""
|
|
59
|
+
if not await self.is_available():
|
|
60
|
+
raise ExecutorNotFoundError(
|
|
61
|
+
"Goose not found. Install: pip install goose-ai"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Build command
|
|
65
|
+
cmd = ["goose", "run"]
|
|
66
|
+
|
|
67
|
+
# Add session if provided
|
|
68
|
+
if request.session_id:
|
|
69
|
+
cmd.extend(["--session", request.session_id])
|
|
70
|
+
|
|
71
|
+
# Add profile if specified
|
|
72
|
+
profile = request.config.get("profile")
|
|
73
|
+
if profile:
|
|
74
|
+
cmd.extend(["--profile", profile])
|
|
75
|
+
|
|
76
|
+
# Add the prompt
|
|
77
|
+
cmd.append(request.prompt)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
process = await asyncio.create_subprocess_exec(
|
|
81
|
+
*cmd,
|
|
82
|
+
cwd=request.working_directory,
|
|
83
|
+
stdout=asyncio.subprocess.PIPE,
|
|
84
|
+
stderr=asyncio.subprocess.PIPE,
|
|
85
|
+
env={**os.environ, **request.environment},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
stdout, stderr = await asyncio.wait_for(
|
|
89
|
+
process.communicate(), timeout=request.timeout_seconds
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return ExecutionResult(
|
|
93
|
+
exit_code=process.returncode,
|
|
94
|
+
stdout=stdout.decode("utf-8", errors="replace"),
|
|
95
|
+
stderr=stderr.decode("utf-8", errors="replace"),
|
|
96
|
+
session_id=request.session_id, # Can be resumed
|
|
97
|
+
duration_seconds=0.0,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
except TimeoutError:
|
|
101
|
+
process.kill()
|
|
102
|
+
raise ExecutorTimeoutError(
|
|
103
|
+
f"Goose execution timed out after {request.timeout_seconds}s"
|
|
104
|
+
)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
raise ExecutorInvocationError(f"Goose execution failed: {str(e)}")
|
|
107
|
+
|
|
108
|
+
async def stream_output(self, request: ExecutionRequest) -> AsyncIterator[str]:
|
|
109
|
+
"""Stream output in real-time."""
|
|
110
|
+
if not await self.is_available():
|
|
111
|
+
raise ExecutorNotFoundError("Goose not found")
|
|
112
|
+
|
|
113
|
+
cmd = ["goose", "run", request.prompt]
|
|
114
|
+
if request.session_id:
|
|
115
|
+
cmd.extend(["--session", request.session_id])
|
|
116
|
+
|
|
117
|
+
process = await asyncio.create_subprocess_exec(
|
|
118
|
+
*cmd,
|
|
119
|
+
cwd=request.working_directory,
|
|
120
|
+
stdout=asyncio.subprocess.PIPE,
|
|
121
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
122
|
+
env={**os.environ, **request.environment},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
while True:
|
|
126
|
+
line = await process.stdout.readline()
|
|
127
|
+
if not line:
|
|
128
|
+
break
|
|
129
|
+
yield line.decode("utf-8", errors="replace")
|
|
130
|
+
|
|
131
|
+
await process.wait()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""OpenCode CLI adapter."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
|
|
8
|
+
from app.executors.registry import ExecutorRegistry
|
|
9
|
+
from app.executors.spec import (
|
|
10
|
+
ExecutionRequest,
|
|
11
|
+
ExecutionResult,
|
|
12
|
+
ExecutorAdapter,
|
|
13
|
+
ExecutorCapability,
|
|
14
|
+
ExecutorInvocationError,
|
|
15
|
+
ExecutorMetadata,
|
|
16
|
+
ExecutorNotFoundError,
|
|
17
|
+
ExecutorTimeoutError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@ExecutorRegistry.register("opencode")
|
|
22
|
+
class OpenCodeAdapter(ExecutorAdapter):
|
|
23
|
+
"""OpenCode CLI adapter for automated code changes."""
|
|
24
|
+
|
|
25
|
+
def get_metadata(self) -> ExecutorMetadata:
|
|
26
|
+
return ExecutorMetadata(
|
|
27
|
+
name="opencode",
|
|
28
|
+
display_name="OpenCode CLI",
|
|
29
|
+
version="1.0.0",
|
|
30
|
+
capabilities=[
|
|
31
|
+
ExecutorCapability.STREAMING_OUTPUT,
|
|
32
|
+
ExecutorCapability.YOLO_MODE,
|
|
33
|
+
],
|
|
34
|
+
config_schema={
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"provider": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"default": "anthropic",
|
|
40
|
+
"description": "LLM provider for OpenCode",
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
documentation_url="https://github.com/opencode-ai/opencode",
|
|
45
|
+
author="OpenCode",
|
|
46
|
+
license="MIT",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def is_available(self) -> bool:
|
|
50
|
+
"""Check if opencode CLI is installed."""
|
|
51
|
+
return shutil.which("opencode") is not None
|
|
52
|
+
|
|
53
|
+
async def execute(self, request: ExecutionRequest) -> ExecutionResult:
|
|
54
|
+
"""Execute using OpenCode CLI."""
|
|
55
|
+
if not await self.is_available():
|
|
56
|
+
raise ExecutorNotFoundError(
|
|
57
|
+
"OpenCode CLI not found. Install from https://github.com/opencode-ai/opencode"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
cmd = ["opencode", "--print"]
|
|
61
|
+
|
|
62
|
+
if request.yolo_mode:
|
|
63
|
+
cmd.append("--yolo")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
process = await asyncio.create_subprocess_exec(
|
|
67
|
+
*cmd,
|
|
68
|
+
cwd=request.working_directory,
|
|
69
|
+
stdin=asyncio.subprocess.PIPE,
|
|
70
|
+
stdout=asyncio.subprocess.PIPE,
|
|
71
|
+
stderr=asyncio.subprocess.PIPE,
|
|
72
|
+
env={**os.environ, **request.environment},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
stdout, stderr = await asyncio.wait_for(
|
|
76
|
+
process.communicate(input=request.prompt.encode("utf-8")),
|
|
77
|
+
timeout=request.timeout_seconds,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return ExecutionResult(
|
|
81
|
+
exit_code=process.returncode,
|
|
82
|
+
stdout=stdout.decode("utf-8", errors="replace"),
|
|
83
|
+
stderr=stderr.decode("utf-8", errors="replace"),
|
|
84
|
+
duration_seconds=0.0,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
except TimeoutError:
|
|
88
|
+
process.kill()
|
|
89
|
+
raise ExecutorTimeoutError(
|
|
90
|
+
f"OpenCode execution timed out after {request.timeout_seconds}s"
|
|
91
|
+
) from None
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise ExecutorInvocationError(f"OpenCode execution failed: {e!s}") from e
|
|
94
|
+
|
|
95
|
+
async def stream_output(self, request: ExecutionRequest) -> AsyncIterator[str]:
|
|
96
|
+
"""Stream output in real-time."""
|
|
97
|
+
if not await self.is_available():
|
|
98
|
+
raise ExecutorNotFoundError("OpenCode CLI not found")
|
|
99
|
+
|
|
100
|
+
cmd = ["opencode", "--print"]
|
|
101
|
+
if request.yolo_mode:
|
|
102
|
+
cmd.append("--yolo")
|
|
103
|
+
|
|
104
|
+
process = await asyncio.create_subprocess_exec(
|
|
105
|
+
*cmd,
|
|
106
|
+
cwd=request.working_directory,
|
|
107
|
+
stdin=asyncio.subprocess.PIPE,
|
|
108
|
+
stdout=asyncio.subprocess.PIPE,
|
|
109
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
110
|
+
env={**os.environ, **request.environment},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
process.stdin.write(request.prompt.encode("utf-8"))
|
|
114
|
+
await process.stdin.drain()
|
|
115
|
+
process.stdin.close()
|
|
116
|
+
|
|
117
|
+
while True:
|
|
118
|
+
line = await process.stdout.readline()
|
|
119
|
+
if not line:
|
|
120
|
+
break
|
|
121
|
+
yield line.decode("utf-8", errors="replace")
|
|
122
|
+
|
|
123
|
+
await process.wait()
|