agentpool 2.1.9__py3-none-any.whl → 2.5.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.
- acp/__init__.py +13 -4
- acp/acp_requests.py +20 -77
- acp/agent/connection.py +8 -0
- acp/agent/implementations/debug_server/debug_server.py +6 -2
- acp/agent/protocol.py +6 -0
- acp/bridge/README.md +15 -2
- acp/bridge/__init__.py +3 -2
- acp/bridge/__main__.py +60 -19
- acp/bridge/ws_server.py +173 -0
- acp/bridge/ws_server_cli.py +89 -0
- acp/client/connection.py +38 -29
- acp/client/implementations/default_client.py +3 -2
- acp/client/implementations/headless_client.py +2 -2
- acp/connection.py +2 -2
- acp/notifications.py +20 -50
- acp/schema/__init__.py +2 -0
- acp/schema/agent_responses.py +21 -0
- acp/schema/client_requests.py +3 -3
- acp/schema/session_state.py +63 -29
- acp/stdio.py +39 -9
- acp/task/supervisor.py +2 -2
- acp/transports.py +362 -2
- acp/utils.py +17 -4
- agentpool/__init__.py +6 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +407 -277
- agentpool/agents/acp_agent/acp_converters.py +196 -38
- agentpool/agents/acp_agent/client_handler.py +191 -26
- agentpool/agents/acp_agent/session_state.py +17 -6
- agentpool/agents/agent.py +607 -572
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +176 -110
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +632 -17
- agentpool/agents/claude_code_agent/FORKING.md +191 -0
- agentpool/agents/claude_code_agent/__init__.py +13 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
- agentpool/agents/claude_code_agent/converters.py +74 -143
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/claude_code_agent/models.py +77 -0
- agentpool/agents/claude_code_agent/static_info.py +100 -0
- agentpool/agents/claude_code_agent/usage.py +242 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +24 -0
- agentpool/agents/events/builtin_handlers.py +67 -1
- agentpool/agents/events/event_emitter.py +32 -2
- agentpool/agents/events/events.py +104 -3
- agentpool/agents/events/infer_info.py +145 -0
- agentpool/agents/events/processors.py +254 -0
- agentpool/agents/interactions.py +41 -6
- agentpool/agents/modes.py +67 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +56 -21
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/acp_assistant.yml +2 -2
- agentpool/config_resources/agents.yml +3 -0
- agentpool/config_resources/agents_template.yml +1 -0
- agentpool/config_resources/claude_code_agent.yml +10 -6
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +136 -289
- agentpool/delegation/team.py +58 -57
- agentpool/delegation/teamrun.py +51 -55
- agentpool/diagnostics/__init__.py +53 -0
- agentpool/diagnostics/lsp_manager.py +1593 -0
- agentpool/diagnostics/lsp_proxy.py +41 -0
- agentpool/diagnostics/lsp_proxy_script.py +229 -0
- agentpool/diagnostics/models.py +398 -0
- agentpool/functional/run.py +10 -4
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +76 -32
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +34 -54
- agentpool/mcp_server/registries/official_registry_client.py +35 -1
- agentpool/mcp_server/tool_bridge.py +186 -139
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/connection_manager.py +11 -10
- agentpool/messaging/event_manager.py +5 -5
- agentpool/messaging/message_container.py +6 -30
- agentpool/messaging/message_history.py +99 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +54 -35
- agentpool/messaging/processing.py +12 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -24
- agentpool/models/acp_agents/mcp_capable.py +126 -157
- agentpool/models/acp_agents/non_mcp.py +129 -95
- agentpool/models/agents.py +98 -76
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +144 -19
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +113 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +11 -1
- agentpool/resource_providers/aggregating.py +56 -5
- agentpool/resource_providers/base.py +70 -4
- agentpool/resource_providers/codemode/code_executor.py +72 -5
- agentpool/resource_providers/codemode/helpers.py +2 -2
- agentpool/resource_providers/codemode/provider.py +64 -12
- agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
- agentpool/resource_providers/codemode/remote_provider.py +9 -12
- agentpool/resource_providers/filtering.py +3 -1
- agentpool/resource_providers/mcp_provider.py +89 -12
- agentpool/resource_providers/plan_provider.py +228 -46
- agentpool/resource_providers/pool.py +7 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +4 -2
- agentpool/sessions/__init__.py +4 -1
- agentpool/sessions/manager.py +33 -5
- agentpool/sessions/models.py +59 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +572 -49
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +538 -20
- agentpool/tool_impls/__init__.py +6 -0
- agentpool/tool_impls/agent_cli/__init__.py +42 -0
- agentpool/tool_impls/agent_cli/tool.py +95 -0
- agentpool/tool_impls/bash/__init__.py +64 -0
- agentpool/tool_impls/bash/helpers.py +35 -0
- agentpool/tool_impls/bash/tool.py +171 -0
- agentpool/tool_impls/delete_path/__init__.py +70 -0
- agentpool/tool_impls/delete_path/tool.py +142 -0
- agentpool/tool_impls/download_file/__init__.py +80 -0
- agentpool/tool_impls/download_file/tool.py +183 -0
- agentpool/tool_impls/execute_code/__init__.py +55 -0
- agentpool/tool_impls/execute_code/tool.py +163 -0
- agentpool/tool_impls/grep/__init__.py +80 -0
- agentpool/tool_impls/grep/tool.py +200 -0
- agentpool/tool_impls/list_directory/__init__.py +73 -0
- agentpool/tool_impls/list_directory/tool.py +197 -0
- agentpool/tool_impls/question/__init__.py +42 -0
- agentpool/tool_impls/question/tool.py +127 -0
- agentpool/tool_impls/read/__init__.py +104 -0
- agentpool/tool_impls/read/tool.py +305 -0
- agentpool/tools/__init__.py +2 -1
- agentpool/tools/base.py +114 -34
- agentpool/tools/manager.py +57 -1
- agentpool/ui/base.py +2 -2
- agentpool/ui/mock_provider.py +2 -2
- agentpool/ui/stdlib_provider.py +2 -2
- agentpool/utils/file_watcher.py +269 -0
- agentpool/utils/identifiers.py +121 -0
- agentpool/utils/pydantic_ai_helpers.py +46 -0
- agentpool/utils/streams.py +616 -2
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
- agentpool-2.5.0.dist-info/RECORD +579 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +24 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +100 -21
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +42 -5
- agentpool_commands/agents.py +75 -2
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/pool.py +260 -0
- agentpool_commands/session.py +1 -1
- agentpool_commands/text_sharing/__init__.py +119 -0
- agentpool_commands/text_sharing/base.py +123 -0
- agentpool_commands/text_sharing/github_gist.py +80 -0
- agentpool_commands/text_sharing/opencode.py +462 -0
- agentpool_commands/text_sharing/paste_rs.py +59 -0
- agentpool_commands/text_sharing/pastebin.py +116 -0
- agentpool_commands/text_sharing/shittycodingagent.py +112 -0
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +80 -30
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/converters.py +1 -1
- agentpool_config/event_handlers.py +42 -0
- agentpool_config/events.py +1 -1
- agentpool_config/forward_targets.py +1 -4
- agentpool_config/jinja.py +3 -3
- agentpool_config/mcp_server.py +132 -6
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +82 -38
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -22
- agentpool_config/toolsets.py +109 -233
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +234 -181
- agentpool_server/acp_server/commands/acp_commands.py +151 -156
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +24 -90
- agentpool_server/acp_server/session.py +173 -331
- agentpool_server/acp_server/session_manager.py +8 -34
- agentpool_server/agui_server/server.py +3 -1
- agentpool_server/mcp_server/server.py +5 -2
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +401 -0
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +19 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +975 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +421 -0
- agentpool_server/opencode_server/models/__init__.py +250 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +72 -0
- agentpool_server/opencode_server/models/base.py +26 -0
- agentpool_server/opencode_server/models/common.py +23 -0
- agentpool_server/opencode_server/models/config.py +37 -0
- agentpool_server/opencode_server/models/events.py +821 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +44 -0
- agentpool_server/opencode_server/models/message.py +179 -0
- agentpool_server/opencode_server/models/parts.py +323 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +111 -0
- agentpool_server/opencode_server/routes/__init__.py +29 -0
- agentpool_server/opencode_server/routes/agent_routes.py +473 -0
- agentpool_server/opencode_server/routes/app_routes.py +202 -0
- agentpool_server/opencode_server/routes/config_routes.py +302 -0
- agentpool_server/opencode_server/routes/file_routes.py +571 -0
- agentpool_server/opencode_server/routes/global_routes.py +94 -0
- agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
- agentpool_server/opencode_server/routes/message_routes.py +761 -0
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +300 -0
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +1276 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +475 -0
- agentpool_server/opencode_server/state.py +151 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +12 -0
- agentpool_storage/base.py +184 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/claude_provider/provider.py +1089 -0
- agentpool_storage/file_provider.py +278 -15
- agentpool_storage/memory_provider.py +193 -12
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
- agentpool_storage/opencode_provider/__init__.py +16 -0
- agentpool_storage/opencode_provider/helpers.py +414 -0
- agentpool_storage/opencode_provider/provider.py +895 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +26 -6
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +269 -3
- agentpool_storage/sql_provider/utils.py +12 -13
- agentpool_storage/zed_provider/__init__.py +16 -0
- agentpool_storage/zed_provider/helpers.py +281 -0
- agentpool_storage/zed_provider/models.py +130 -0
- agentpool_storage/zed_provider/provider.py +442 -0
- agentpool_storage/zed_provider.py +803 -0
- agentpool_toolsets/__init__.py +0 -2
- agentpool_toolsets/builtin/__init__.py +2 -12
- agentpool_toolsets/builtin/code.py +96 -57
- agentpool_toolsets/builtin/debug.py +118 -48
- agentpool_toolsets/builtin/execution_environment.py +115 -230
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +9 -4
- agentpool_toolsets/builtin/subagent_tools.py +64 -51
- agentpool_toolsets/builtin/workers.py +4 -2
- agentpool_toolsets/composio_toolset.py +2 -2
- agentpool_toolsets/entry_points.py +3 -1
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +99 -7
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +511 -0
- agentpool_toolsets/mcp_run_toolset.py +87 -12
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- agentpool-2.1.9.dist-info/RECORD +0 -474
- agentpool_config/resources.py +0 -33
- agentpool_server/acp_server/acp_tools.py +0 -43
- agentpool_server/acp_server/commands/spawn.py +0 -210
- agentpool_storage/text_log_provider.py +0 -275
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""Agent, command, MCP, LSP, formatter, and logging routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException
|
|
8
|
+
import httpx
|
|
9
|
+
from llmling_models.auth.anthropic_auth import (
|
|
10
|
+
AnthropicTokenStore,
|
|
11
|
+
build_authorization_url,
|
|
12
|
+
exchange_code_for_token,
|
|
13
|
+
generate_pkce,
|
|
14
|
+
)
|
|
15
|
+
from pydantic import BaseModel, HttpUrl
|
|
16
|
+
|
|
17
|
+
from agentpool.mcp_server.manager import MCPManager
|
|
18
|
+
from agentpool.resource_providers import AggregatingResourceProvider
|
|
19
|
+
from agentpool_config.mcp_server import (
|
|
20
|
+
SSEMCPServerConfig,
|
|
21
|
+
StdioMCPServerConfig,
|
|
22
|
+
StreamableHTTPMCPServerConfig,
|
|
23
|
+
)
|
|
24
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
25
|
+
from agentpool_server.opencode_server.models import (
|
|
26
|
+
Agent,
|
|
27
|
+
Command,
|
|
28
|
+
LogRequest,
|
|
29
|
+
McpResource,
|
|
30
|
+
MCPStatus,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
router = APIRouter(tags=["agent"])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.get("/agent")
|
|
38
|
+
async def list_agents(state: StateDep) -> list[Agent]:
|
|
39
|
+
"""List available agents from the AgentPool.
|
|
40
|
+
|
|
41
|
+
Returns all agents with their configurations, suitable for the agent
|
|
42
|
+
switcher UI. Agents are marked as primary (visible in switcher) or
|
|
43
|
+
subagent (hidden, used internally).
|
|
44
|
+
"""
|
|
45
|
+
if state.agent.agent_pool is None:
|
|
46
|
+
return [
|
|
47
|
+
Agent(
|
|
48
|
+
name="default",
|
|
49
|
+
description="Default AgentPool agent",
|
|
50
|
+
mode="primary",
|
|
51
|
+
default=True,
|
|
52
|
+
)
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
pool = state.agent.agent_pool
|
|
56
|
+
agents: list[Agent] = []
|
|
57
|
+
first_agent_name = next(iter(pool.all_agents.keys()), None)
|
|
58
|
+
|
|
59
|
+
for name, agent in pool.all_agents.items():
|
|
60
|
+
# Get description from agent
|
|
61
|
+
agents.append(
|
|
62
|
+
Agent(
|
|
63
|
+
name=name,
|
|
64
|
+
description=agent.description or f"Agent: {name}",
|
|
65
|
+
# model=AgentModel(model_id=agent.model_name or "unknown", provider_id=""),
|
|
66
|
+
mode="primary", # All agents visible for now; add hidden config later
|
|
67
|
+
default=(name == first_agent_name), # First agent is default
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
agents
|
|
73
|
+
if agents
|
|
74
|
+
else [Agent(name="default", description="Default agent", mode="primary", default=True)]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@router.get("/command")
|
|
79
|
+
async def list_commands(state: StateDep) -> list[Command]:
|
|
80
|
+
"""List available slash commands.
|
|
81
|
+
|
|
82
|
+
Commands are derived from MCP prompts available to the agent.
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
prompts = await state.agent.tools.list_prompts()
|
|
86
|
+
return [Command(name=p.name, description=p.description or "") for p in prompts]
|
|
87
|
+
except Exception: # noqa: BLE001
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@router.get("/mcp")
|
|
92
|
+
async def get_mcp_status(state: StateDep) -> dict[str, MCPStatus]:
|
|
93
|
+
"""Get MCP server status.
|
|
94
|
+
|
|
95
|
+
Returns status for each connected MCP server.
|
|
96
|
+
"""
|
|
97
|
+
# Use agent's get_mcp_server_info method which handles different agent types
|
|
98
|
+
server_info = state.agent.get_mcp_server_info()
|
|
99
|
+
|
|
100
|
+
# Convert MCPServerStatus dataclass to MCPStatus response model
|
|
101
|
+
return {
|
|
102
|
+
name: MCPStatus(
|
|
103
|
+
name=status.name,
|
|
104
|
+
status=status.status,
|
|
105
|
+
error=status.error,
|
|
106
|
+
)
|
|
107
|
+
for name, status in server_info.items()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AddMCPServerRequest(BaseModel):
|
|
112
|
+
"""Request to add an MCP server dynamically."""
|
|
113
|
+
|
|
114
|
+
command: str | None = None
|
|
115
|
+
"""Command to run (for stdio servers)."""
|
|
116
|
+
|
|
117
|
+
args: list[str] | None = None
|
|
118
|
+
"""Arguments for the command."""
|
|
119
|
+
|
|
120
|
+
url: str | None = None
|
|
121
|
+
"""URL for HTTP/SSE servers."""
|
|
122
|
+
|
|
123
|
+
env: dict[str, str] | None = None
|
|
124
|
+
"""Environment variables for the server."""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@router.post("/mcp")
|
|
128
|
+
async def add_mcp_server(request: AddMCPServerRequest, state: StateDep) -> MCPStatus:
|
|
129
|
+
"""Add an MCP server dynamically.
|
|
130
|
+
|
|
131
|
+
Supports stdio servers (command + args) or HTTP/SSE servers (url).
|
|
132
|
+
"""
|
|
133
|
+
# Build the config based on request
|
|
134
|
+
# Note: client_id is auto-generated from command/url, custom names not supported
|
|
135
|
+
config: SSEMCPServerConfig | StdioMCPServerConfig | StreamableHTTPMCPServerConfig
|
|
136
|
+
if request.url:
|
|
137
|
+
# HTTP-based server
|
|
138
|
+
if request.url.endswith("/sse"):
|
|
139
|
+
config = SSEMCPServerConfig(url=HttpUrl(request.url))
|
|
140
|
+
else:
|
|
141
|
+
config = StreamableHTTPMCPServerConfig(url=HttpUrl(request.url))
|
|
142
|
+
elif request.command: # Stdio server
|
|
143
|
+
args = request.args or []
|
|
144
|
+
config = StdioMCPServerConfig(command=request.command, args=args, env=request.env)
|
|
145
|
+
else:
|
|
146
|
+
detail = "Must provide either 'command' (for stdio) or 'url' (for HTTP/SSE)"
|
|
147
|
+
raise HTTPException(status_code=400, detail=detail)
|
|
148
|
+
|
|
149
|
+
# Find the MCPManager and add the server
|
|
150
|
+
manager: MCPManager | None = None
|
|
151
|
+
for provider in state.agent.tools.external_providers:
|
|
152
|
+
if isinstance(provider, MCPManager):
|
|
153
|
+
manager = provider
|
|
154
|
+
break
|
|
155
|
+
if isinstance(provider, AggregatingResourceProvider):
|
|
156
|
+
for nested in provider.providers:
|
|
157
|
+
if isinstance(nested, MCPManager):
|
|
158
|
+
manager = nested
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
if manager is None:
|
|
162
|
+
raise HTTPException(status_code=400, detail="No MCP manager available")
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
await manager.setup_server(config, add_to_config=True)
|
|
166
|
+
return MCPStatus(name=config.client_id, status="connected")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
raise HTTPException(status_code=500, detail=f"Failed to add MCP server: {e}") from e
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@router.post("/log")
|
|
172
|
+
async def log(request: LogRequest, state: StateDep) -> bool:
|
|
173
|
+
"""Write a log entry.
|
|
174
|
+
|
|
175
|
+
TODO: Integrate with proper logging.
|
|
176
|
+
"""
|
|
177
|
+
_ = state # unused for now
|
|
178
|
+
print(f"[{request.level}] {request.service}: {request.message}")
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@router.get("/experimental/resource")
|
|
183
|
+
async def list_mcp_resources(state: StateDep) -> dict[str, McpResource]:
|
|
184
|
+
"""Get all available MCP resources from connected servers.
|
|
185
|
+
|
|
186
|
+
Returns a dictionary mapping resource keys to McpResource objects.
|
|
187
|
+
Keys are formatted as "{client}:{resource_name}" for uniqueness.
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
resources = await state.agent.tools.list_resources()
|
|
191
|
+
result: dict[str, McpResource] = {}
|
|
192
|
+
|
|
193
|
+
for resource in resources:
|
|
194
|
+
# Create unique key: sanitize client and resource names
|
|
195
|
+
client_name = (resource.client or "unknown").replace("/", "_")
|
|
196
|
+
resource_name = resource.name.replace("/", "_")
|
|
197
|
+
key = f"{client_name}:{resource_name}"
|
|
198
|
+
|
|
199
|
+
result[key] = McpResource(
|
|
200
|
+
name=resource.name,
|
|
201
|
+
uri=resource.uri,
|
|
202
|
+
description=resource.description,
|
|
203
|
+
mime_type=resource.mime_type,
|
|
204
|
+
client=resource.client or "unknown",
|
|
205
|
+
)
|
|
206
|
+
except Exception: # noqa: BLE001
|
|
207
|
+
return {}
|
|
208
|
+
else:
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@router.get("/experimental/tool/ids")
|
|
213
|
+
async def list_tool_ids(state: StateDep) -> list[str]:
|
|
214
|
+
"""List all available tool IDs.
|
|
215
|
+
|
|
216
|
+
Returns a list of tool names that are available to the agent.
|
|
217
|
+
OpenCode expects: Array<string>
|
|
218
|
+
"""
|
|
219
|
+
try:
|
|
220
|
+
tools = await state.agent.tools.get_tools()
|
|
221
|
+
return [tool.name for tool in tools]
|
|
222
|
+
except Exception: # noqa: BLE001
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class ToolListItem(BaseModel):
|
|
227
|
+
"""Tool info matching OpenCode SDK ToolListItem type."""
|
|
228
|
+
|
|
229
|
+
id: str
|
|
230
|
+
description: str
|
|
231
|
+
parameters: dict[str, Any]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@router.get("/experimental/tool")
|
|
235
|
+
async def list_tools_with_schemas( # noqa: D417
|
|
236
|
+
state: StateDep,
|
|
237
|
+
provider: str | None = None,
|
|
238
|
+
model: str | None = None,
|
|
239
|
+
) -> list[ToolListItem]:
|
|
240
|
+
"""List tools with their JSON schemas.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
provider: Optional provider filter (not used currently)
|
|
244
|
+
model: Optional model filter (not used currently)
|
|
245
|
+
|
|
246
|
+
Returns list of tools matching OpenCode's ToolListItem format:
|
|
247
|
+
- id: string
|
|
248
|
+
- description: string
|
|
249
|
+
- parameters: unknown (JSON schema)
|
|
250
|
+
"""
|
|
251
|
+
_ = provider, model # Currently unused, for future filtering
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
tools = await state.agent.tools.get_tools()
|
|
255
|
+
result = []
|
|
256
|
+
for tool in tools:
|
|
257
|
+
# Extract parameters schema from the OpenAI function schema
|
|
258
|
+
schema = tool.schema
|
|
259
|
+
params = schema.get("function", {}).get("parameters", {})
|
|
260
|
+
item = ToolListItem(id=tool.name, description=tool.description or "", parameters=params)
|
|
261
|
+
result.append(item)
|
|
262
|
+
except Exception: # noqa: BLE001
|
|
263
|
+
return []
|
|
264
|
+
else:
|
|
265
|
+
return result
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@router.get("/lsp")
|
|
269
|
+
async def get_lsp_status(state: StateDep) -> list[dict[str, Any]]:
|
|
270
|
+
"""Get LSP server status.
|
|
271
|
+
|
|
272
|
+
Returns status of all running LSP servers.
|
|
273
|
+
"""
|
|
274
|
+
try:
|
|
275
|
+
lsp_manager = state.get_or_create_lsp_manager()
|
|
276
|
+
servers = []
|
|
277
|
+
for server_id, server_state in lsp_manager._servers.items():
|
|
278
|
+
# OpenCode TUI expects "connected" or "error" for status colors
|
|
279
|
+
status = "connected" if server_state.initialized else "error"
|
|
280
|
+
servers.append({
|
|
281
|
+
"id": server_id,
|
|
282
|
+
"name": server_id,
|
|
283
|
+
"status": status,
|
|
284
|
+
"language": server_state.language,
|
|
285
|
+
"root": server_state.root_uri, # TUI uses "root" not "rootUri"
|
|
286
|
+
})
|
|
287
|
+
except Exception: # noqa: BLE001
|
|
288
|
+
return []
|
|
289
|
+
else:
|
|
290
|
+
return servers
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@router.get("/formatter")
|
|
294
|
+
async def get_formatter_status(state: StateDep) -> list[dict[str, Any]]:
|
|
295
|
+
"""Get formatter status.
|
|
296
|
+
|
|
297
|
+
Returns empty list - formatters not supported yet.
|
|
298
|
+
"""
|
|
299
|
+
_ = state
|
|
300
|
+
return []
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@router.get("/provider/auth")
|
|
304
|
+
async def get_provider_auth(state: StateDep) -> dict[str, list[dict[str, Any]]]:
|
|
305
|
+
"""Get provider authentication methods.
|
|
306
|
+
|
|
307
|
+
Returns available OAuth providers with their auth methods.
|
|
308
|
+
"""
|
|
309
|
+
_ = state
|
|
310
|
+
return {
|
|
311
|
+
"anthropic": [
|
|
312
|
+
{
|
|
313
|
+
"type": "oauth",
|
|
314
|
+
"label": "Connect Claude Max/Pro",
|
|
315
|
+
"method": "code",
|
|
316
|
+
}
|
|
317
|
+
],
|
|
318
|
+
"copilot": [
|
|
319
|
+
{
|
|
320
|
+
"type": "oauth",
|
|
321
|
+
"label": "Connect GitHub Copilot",
|
|
322
|
+
"method": "device_code",
|
|
323
|
+
}
|
|
324
|
+
],
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# Store for active OAuth flows (in production, use Redis or similar)
|
|
329
|
+
_oauth_flows: dict[str, dict[str, Any]] = {}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@router.post("/provider/{provider_id}/oauth/authorize")
|
|
333
|
+
async def oauth_authorize(provider_id: str, state: StateDep) -> dict[str, Any]:
|
|
334
|
+
"""Start OAuth authorization flow for a provider.
|
|
335
|
+
|
|
336
|
+
Returns URL and instructions for the user to complete authorization.
|
|
337
|
+
"""
|
|
338
|
+
_ = state
|
|
339
|
+
|
|
340
|
+
if provider_id == "anthropic":
|
|
341
|
+
verifier, challenge = generate_pkce()
|
|
342
|
+
auth_url = build_authorization_url(verifier, challenge)
|
|
343
|
+
|
|
344
|
+
# Store verifier for callback
|
|
345
|
+
_oauth_flows[f"anthropic:{verifier}"] = {"verifier": verifier}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
"url": auth_url,
|
|
349
|
+
"instructions": "Sign in with your Anthropic account and copy the authorization code",
|
|
350
|
+
"method": "code",
|
|
351
|
+
"state": verifier,
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if provider_id == "copilot":
|
|
355
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
356
|
+
resp = await client.post(
|
|
357
|
+
"https://github.com/login/device/code",
|
|
358
|
+
headers={
|
|
359
|
+
"accept": "application/json",
|
|
360
|
+
"editor-version": "Neovim/0.6.1",
|
|
361
|
+
"editor-plugin-version": "copilot.vim/1.16.0",
|
|
362
|
+
"content-type": "application/json",
|
|
363
|
+
"user-agent": "GithubCopilot/1.155.0",
|
|
364
|
+
},
|
|
365
|
+
json={"client_id": "Iv1.b507a08c87ecfe98", "scope": "read:user"},
|
|
366
|
+
)
|
|
367
|
+
resp.raise_for_status()
|
|
368
|
+
data = resp.json()
|
|
369
|
+
|
|
370
|
+
device_code = data["device_code"]
|
|
371
|
+
user_code = data["user_code"]
|
|
372
|
+
verification_uri = data["verification_uri"]
|
|
373
|
+
|
|
374
|
+
# Store device_code for callback
|
|
375
|
+
_oauth_flows[f"copilot:{device_code}"] = {"device_code": device_code}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
"url": verification_uri,
|
|
379
|
+
"instructions": f"Enter code: {user_code}",
|
|
380
|
+
"method": "device_code",
|
|
381
|
+
"user_code": user_code,
|
|
382
|
+
"device_code": device_code,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
raise HTTPException(status_code=404, detail=f"Unknown provider: {provider_id}")
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@router.post("/provider/{provider_id}/oauth/callback")
|
|
389
|
+
async def oauth_callback(
|
|
390
|
+
provider_id: str,
|
|
391
|
+
state: StateDep,
|
|
392
|
+
code: str | None = None,
|
|
393
|
+
device_code: str | None = None,
|
|
394
|
+
verifier: str | None = None,
|
|
395
|
+
) -> dict[str, Any]:
|
|
396
|
+
"""Handle OAuth callback/code exchange.
|
|
397
|
+
|
|
398
|
+
For Anthropic: exchanges authorization code for tokens.
|
|
399
|
+
For Copilot: polls for token using device code.
|
|
400
|
+
"""
|
|
401
|
+
_ = state
|
|
402
|
+
|
|
403
|
+
if provider_id == "anthropic":
|
|
404
|
+
if not code or not verifier:
|
|
405
|
+
raise HTTPException(
|
|
406
|
+
status_code=400, detail="Missing code or verifier for Anthropic OAuth"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
token = exchange_code_for_token(code, verifier)
|
|
411
|
+
# Save token
|
|
412
|
+
store = AnthropicTokenStore()
|
|
413
|
+
store.save(token)
|
|
414
|
+
|
|
415
|
+
# Clean up flow state
|
|
416
|
+
_oauth_flows.pop(f"anthropic:{verifier}", None)
|
|
417
|
+
except Exception as e:
|
|
418
|
+
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
419
|
+
else:
|
|
420
|
+
return {
|
|
421
|
+
"type": "success",
|
|
422
|
+
"access": token.access_token,
|
|
423
|
+
"refresh": token.refresh_token,
|
|
424
|
+
"expires": token.expires_at,
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if provider_id == "copilot":
|
|
428
|
+
if not device_code:
|
|
429
|
+
raise HTTPException(
|
|
430
|
+
status_code=400,
|
|
431
|
+
detail="Missing device_code for Copilot OAuth",
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
435
|
+
resp = await client.post(
|
|
436
|
+
"https://github.com/login/oauth/access_token",
|
|
437
|
+
headers={
|
|
438
|
+
"accept": "application/json",
|
|
439
|
+
"editor-version": "Neovim/0.6.1",
|
|
440
|
+
"editor-plugin-version": "copilot.vim/1.16.0",
|
|
441
|
+
"content-type": "application/json",
|
|
442
|
+
"user-agent": "GithubCopilot/1.155.0",
|
|
443
|
+
},
|
|
444
|
+
json={
|
|
445
|
+
"client_id": "Iv1.b507a08c87ecfe98",
|
|
446
|
+
"device_code": device_code,
|
|
447
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
448
|
+
},
|
|
449
|
+
)
|
|
450
|
+
data = resp.json()
|
|
451
|
+
|
|
452
|
+
if "error" in data:
|
|
453
|
+
if data["error"] == "authorization_pending":
|
|
454
|
+
return {"type": "pending", "message": "Waiting for user authorization"}
|
|
455
|
+
raise HTTPException(
|
|
456
|
+
status_code=400,
|
|
457
|
+
detail=data.get("error_description", data["error"]),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
access_token = data.get("access_token")
|
|
461
|
+
if access_token:
|
|
462
|
+
# Clean up flow state
|
|
463
|
+
_oauth_flows.pop(f"copilot:{device_code}", None)
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
"type": "success",
|
|
467
|
+
"access": access_token,
|
|
468
|
+
"refresh": data.get("refresh_token"),
|
|
469
|
+
"expires": None, # Copilot tokens don't expire the same way
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {"type": "pending", "message": "No token received yet"}
|
|
473
|
+
raise HTTPException(status_code=404, detail=f"Unknown provider: {provider_id}")
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""App, project, path, and VCS routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, HTTPException
|
|
10
|
+
|
|
11
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
12
|
+
from agentpool_server.opencode_server.models import (
|
|
13
|
+
App,
|
|
14
|
+
AppTimeInfo,
|
|
15
|
+
PathInfo,
|
|
16
|
+
Project,
|
|
17
|
+
ProjectTime,
|
|
18
|
+
ProjectUpdatedEvent,
|
|
19
|
+
ProjectUpdateRequest,
|
|
20
|
+
VcsInfo,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from agentpool.sessions.models import ProjectData
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
router = APIRouter(tags=["app"])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.get("/app")
|
|
32
|
+
async def get_app(state: StateDep) -> App:
|
|
33
|
+
"""Get app information."""
|
|
34
|
+
working_path = Path(state.working_dir)
|
|
35
|
+
return App(
|
|
36
|
+
git=(working_path / ".git").is_dir(),
|
|
37
|
+
hostname="localhost",
|
|
38
|
+
path=PathInfo(
|
|
39
|
+
config="",
|
|
40
|
+
cwd=state.working_dir,
|
|
41
|
+
data="",
|
|
42
|
+
root=state.working_dir,
|
|
43
|
+
state="",
|
|
44
|
+
),
|
|
45
|
+
time=AppTimeInfo(initialized=state.start_time),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _project_data_to_response(data: ProjectData) -> Project:
|
|
50
|
+
"""Convert ProjectData to OpenCode Project response."""
|
|
51
|
+
working_path = Path(data.worktree)
|
|
52
|
+
vcs_dir: str | None = None
|
|
53
|
+
if data.vcs == "git":
|
|
54
|
+
vcs_dir = str(working_path / ".git")
|
|
55
|
+
elif data.vcs == "hg":
|
|
56
|
+
vcs_dir = str(working_path / ".hg")
|
|
57
|
+
|
|
58
|
+
return Project(
|
|
59
|
+
id=data.project_id,
|
|
60
|
+
worktree=data.worktree,
|
|
61
|
+
vcs_dir=vcs_dir,
|
|
62
|
+
vcs=data.vcs,
|
|
63
|
+
time=ProjectTime(created=int(data.created_at.timestamp() * 1000)),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def _get_current_project(state: StateDep) -> ProjectData:
|
|
68
|
+
"""Get or create the current project from storage."""
|
|
69
|
+
from agentpool_storage.project_store import ProjectStore
|
|
70
|
+
|
|
71
|
+
storage = state.pool.storage
|
|
72
|
+
project_store = ProjectStore(storage)
|
|
73
|
+
return await project_store.get_or_create(state.working_dir)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@router.get("/project")
|
|
77
|
+
async def list_projects(state: StateDep) -> list[Project]:
|
|
78
|
+
"""List all projects."""
|
|
79
|
+
from agentpool_storage.project_store import ProjectStore
|
|
80
|
+
|
|
81
|
+
storage = state.pool.storage
|
|
82
|
+
project_store = ProjectStore(storage)
|
|
83
|
+
projects = await project_store.list_recent(limit=50)
|
|
84
|
+
return [_project_data_to_response(p) for p in projects]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@router.get("/project/current")
|
|
88
|
+
async def get_project_current(state: StateDep) -> Project:
|
|
89
|
+
"""Get current project."""
|
|
90
|
+
project = await _get_current_project(state)
|
|
91
|
+
return _project_data_to_response(project)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@router.patch("/project/{project_id}")
|
|
95
|
+
async def update_project(
|
|
96
|
+
project_id: str,
|
|
97
|
+
update: ProjectUpdateRequest,
|
|
98
|
+
state: StateDep,
|
|
99
|
+
) -> Project:
|
|
100
|
+
"""Update project metadata (name, settings).
|
|
101
|
+
|
|
102
|
+
Emits a project.updated event when successful.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
project_id: Project identifier
|
|
106
|
+
update: Fields to update (name and/or settings)
|
|
107
|
+
state: Server state
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Updated project data
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
HTTPException: If project not found
|
|
114
|
+
"""
|
|
115
|
+
from agentpool_storage.project_store import ProjectStore
|
|
116
|
+
|
|
117
|
+
store = ProjectStore(state.pool.storage)
|
|
118
|
+
project_data = None
|
|
119
|
+
|
|
120
|
+
# Update name if provided
|
|
121
|
+
if update.name is not None:
|
|
122
|
+
project_data = await store.set_name(project_id, update.name)
|
|
123
|
+
if not project_data:
|
|
124
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
125
|
+
|
|
126
|
+
# Update settings if provided
|
|
127
|
+
if update.settings:
|
|
128
|
+
if project_data:
|
|
129
|
+
# Already fetched from set_name, update with settings
|
|
130
|
+
project_data = await store.update_settings(project_id, **update.settings)
|
|
131
|
+
else:
|
|
132
|
+
project_data = await store.update_settings(project_id, **update.settings)
|
|
133
|
+
|
|
134
|
+
if not project_data:
|
|
135
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
136
|
+
|
|
137
|
+
# If neither name nor settings provided, just fetch the project
|
|
138
|
+
if not project_data:
|
|
139
|
+
project_data = await store.get_by_id(project_id)
|
|
140
|
+
if not project_data:
|
|
141
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
142
|
+
|
|
143
|
+
# Convert to OpenCode Project model
|
|
144
|
+
project = _project_data_to_response(project_data)
|
|
145
|
+
|
|
146
|
+
# Broadcast event
|
|
147
|
+
await state.broadcast_event(ProjectUpdatedEvent.create(project))
|
|
148
|
+
|
|
149
|
+
return project
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@router.get("/path")
|
|
153
|
+
async def get_path(state: StateDep) -> PathInfo:
|
|
154
|
+
"""Get current path info."""
|
|
155
|
+
return PathInfo(
|
|
156
|
+
config="",
|
|
157
|
+
cwd=state.working_dir,
|
|
158
|
+
data="",
|
|
159
|
+
root=state.working_dir,
|
|
160
|
+
state="",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@router.get("/vcs")
|
|
165
|
+
async def get_vcs(state: StateDep) -> VcsInfo:
|
|
166
|
+
"""Get VCS info.
|
|
167
|
+
|
|
168
|
+
TODO: For remote/ACP support, these git commands should run through
|
|
169
|
+
state.env.execute_command() instead of subprocess.run() so they
|
|
170
|
+
execute on the client side where the repository lives.
|
|
171
|
+
"""
|
|
172
|
+
git_dir = Path(state.working_dir) / ".git"
|
|
173
|
+
if not git_dir.is_dir():
|
|
174
|
+
return VcsInfo(branch=None, dirty=False, commit=None)
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
branch = subprocess.run(
|
|
178
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
179
|
+
cwd=state.working_dir,
|
|
180
|
+
capture_output=True,
|
|
181
|
+
text=True,
|
|
182
|
+
check=True,
|
|
183
|
+
).stdout.strip()
|
|
184
|
+
commit = subprocess.run(
|
|
185
|
+
["git", "rev-parse", "HEAD"],
|
|
186
|
+
cwd=state.working_dir,
|
|
187
|
+
capture_output=True,
|
|
188
|
+
text=True,
|
|
189
|
+
check=True,
|
|
190
|
+
).stdout.strip()
|
|
191
|
+
dirty = bool(
|
|
192
|
+
subprocess.run(
|
|
193
|
+
["git", "status", "--porcelain"],
|
|
194
|
+
cwd=state.working_dir,
|
|
195
|
+
capture_output=True,
|
|
196
|
+
text=True,
|
|
197
|
+
check=True,
|
|
198
|
+
).stdout.strip()
|
|
199
|
+
)
|
|
200
|
+
return VcsInfo(branch=branch, dirty=dirty, commit=commit)
|
|
201
|
+
except subprocess.CalledProcessError:
|
|
202
|
+
return VcsInfo(branch=None, dirty=False, commit=None)
|