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