crabcode 0.1.6__py3-none-win_amd64.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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- crabcode_search/tool.py +268 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""MCP client — connects to MCP servers and loads tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from crabcode_core.logging_utils import get_logger
|
|
12
|
+
from crabcode_core.types.config import McpServerConfig
|
|
13
|
+
from crabcode_core.types.tool import Tool, ToolContext, ToolResult
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _normalize_name(name: str) -> str:
|
|
19
|
+
"""Normalize a server/tool name to a valid identifier."""
|
|
20
|
+
return re.sub(r'[^a-zA-Z0-9_]', '_', name)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_mcp_tool_name(server_name: str, tool_name: str) -> str:
|
|
24
|
+
"""Build the prefixed MCP tool name."""
|
|
25
|
+
return f"mcp__{_normalize_name(server_name)}__{_normalize_name(tool_name)}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class McpToolWrapper(Tool):
|
|
29
|
+
"""Wraps an MCP tool as a CrabCode Tool."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
server_name: str,
|
|
34
|
+
tool_name: str,
|
|
35
|
+
description: str,
|
|
36
|
+
schema: dict[str, Any],
|
|
37
|
+
client: Any,
|
|
38
|
+
):
|
|
39
|
+
self.name = build_mcp_tool_name(server_name, tool_name)
|
|
40
|
+
self.description = description
|
|
41
|
+
self.input_schema = schema
|
|
42
|
+
self.is_read_only = False
|
|
43
|
+
self.is_concurrency_safe = True
|
|
44
|
+
self.is_enabled = True
|
|
45
|
+
|
|
46
|
+
self._server_name = server_name
|
|
47
|
+
self._original_name = tool_name
|
|
48
|
+
self._client = client
|
|
49
|
+
|
|
50
|
+
async def call(
|
|
51
|
+
self,
|
|
52
|
+
tool_input: dict[str, Any],
|
|
53
|
+
context: ToolContext,
|
|
54
|
+
) -> ToolResult:
|
|
55
|
+
try:
|
|
56
|
+
result = await self._client.call_tool(self._original_name, tool_input)
|
|
57
|
+
|
|
58
|
+
text_parts: list[str] = []
|
|
59
|
+
if hasattr(result, "content"):
|
|
60
|
+
for block in result.content:
|
|
61
|
+
if hasattr(block, "text"):
|
|
62
|
+
text_parts.append(block.text)
|
|
63
|
+
|
|
64
|
+
output = "\n".join(text_parts) if text_parts else str(result)
|
|
65
|
+
is_error = getattr(result, "isError", False)
|
|
66
|
+
|
|
67
|
+
return ToolResult(
|
|
68
|
+
data=result,
|
|
69
|
+
result_for_model=output,
|
|
70
|
+
is_error=is_error,
|
|
71
|
+
)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return ToolResult(
|
|
74
|
+
result_for_model=f"MCP tool error: {e}",
|
|
75
|
+
is_error=True,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class McpManager:
|
|
80
|
+
"""Manages MCP server connections and tool discovery."""
|
|
81
|
+
|
|
82
|
+
def __init__(self) -> None:
|
|
83
|
+
self._connections: dict[str, Any] = {}
|
|
84
|
+
self._tools: list[McpToolWrapper] = []
|
|
85
|
+
|
|
86
|
+
async def connect(
|
|
87
|
+
self,
|
|
88
|
+
servers: dict[str, McpServerConfig],
|
|
89
|
+
) -> list[McpToolWrapper]:
|
|
90
|
+
"""Connect to all configured MCP servers and fetch tools."""
|
|
91
|
+
all_tools: list[McpToolWrapper] = []
|
|
92
|
+
|
|
93
|
+
for name, config in servers.items():
|
|
94
|
+
if config.disabled:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
tools = await self._connect_server(name, config)
|
|
99
|
+
all_tools.extend(tools)
|
|
100
|
+
except Exception:
|
|
101
|
+
logger.exception("Failed to connect MCP server: %s", name)
|
|
102
|
+
|
|
103
|
+
self._tools = all_tools
|
|
104
|
+
return all_tools
|
|
105
|
+
|
|
106
|
+
async def _connect_server(
|
|
107
|
+
self,
|
|
108
|
+
name: str,
|
|
109
|
+
config: McpServerConfig,
|
|
110
|
+
) -> list[McpToolWrapper]:
|
|
111
|
+
"""Connect to a single MCP server."""
|
|
112
|
+
try:
|
|
113
|
+
from mcp import ClientSession, StdioServerParameters
|
|
114
|
+
from mcp.client.stdio import stdio_client
|
|
115
|
+
except ImportError:
|
|
116
|
+
logger.warning("MCP package not installed; skipping server %s", name)
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
if not config.command:
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
env = {**os.environ, **config.env}
|
|
123
|
+
server_params = StdioServerParameters(
|
|
124
|
+
command=config.command[0],
|
|
125
|
+
args=config.command[1:] if len(config.command) > 1 else [],
|
|
126
|
+
env=env,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
read_stream, write_stream = await asyncio.wait_for(
|
|
131
|
+
stdio_client(server_params).__aenter__(),
|
|
132
|
+
timeout=30,
|
|
133
|
+
)
|
|
134
|
+
session = ClientSession(read_stream, write_stream)
|
|
135
|
+
initialization = await session.initialize()
|
|
136
|
+
self._connections[name] = session
|
|
137
|
+
|
|
138
|
+
tools_response = await session.list_tools()
|
|
139
|
+
tools: list[McpToolWrapper] = []
|
|
140
|
+
for tool_info in tools_response.tools:
|
|
141
|
+
wrapper = McpToolWrapper(
|
|
142
|
+
server_name=name,
|
|
143
|
+
tool_name=tool_info.name,
|
|
144
|
+
description=tool_info.description or "",
|
|
145
|
+
schema=tool_info.inputSchema or {"type": "object", "properties": {}},
|
|
146
|
+
client=session,
|
|
147
|
+
)
|
|
148
|
+
tools.append(wrapper)
|
|
149
|
+
wrapper.server_instructions = getattr(initialization, "instructions", None) or ""
|
|
150
|
+
|
|
151
|
+
return tools
|
|
152
|
+
|
|
153
|
+
except Exception:
|
|
154
|
+
logger.exception("MCP connection failed for server %s", name)
|
|
155
|
+
return []
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def tools(self) -> list[McpToolWrapper]:
|
|
159
|
+
return self._tools
|
|
160
|
+
|
|
161
|
+
async def disconnect_all(self) -> None:
|
|
162
|
+
"""Disconnect from all MCP servers."""
|
|
163
|
+
for session in self._connections.values():
|
|
164
|
+
try:
|
|
165
|
+
await session.__aexit__(None, None, None)
|
|
166
|
+
except Exception:
|
|
167
|
+
logger.exception("Failed to disconnect MCP session cleanly")
|
|
168
|
+
self._connections.clear()
|
|
169
|
+
self._tools.clear()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""MCP configuration — multi-scope server config loading and merging."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from crabcode_core.logging_utils import get_logger
|
|
10
|
+
from crabcode_core.paths import get_config_home
|
|
11
|
+
from crabcode_core.types.config import McpServerConfig
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_mcp_configs(cwd: str) -> dict[str, McpServerConfig]:
|
|
17
|
+
"""Load and merge MCP server configurations from multiple scopes.
|
|
18
|
+
|
|
19
|
+
Scopes (later overrides earlier):
|
|
20
|
+
1. User ($CRABCODE_HOME/mcp_servers.json; default ~/.crabcode)
|
|
21
|
+
2. Project (<project>/.crabcode/mcp_servers.json)
|
|
22
|
+
3. Local (<project>/.crabcode/mcp_servers.local.json)
|
|
23
|
+
"""
|
|
24
|
+
merged: dict[str, dict[str, Any]] = {}
|
|
25
|
+
home = Path.home()
|
|
26
|
+
|
|
27
|
+
paths = [
|
|
28
|
+
get_config_home() / "mcp_servers.json",
|
|
29
|
+
home / ".claude" / "mcp_servers.json",
|
|
30
|
+
Path(cwd).resolve() / ".crabcode" / "mcp_servers.json",
|
|
31
|
+
Path(cwd).resolve() / ".claude" / "mcp_servers.json",
|
|
32
|
+
Path(cwd).resolve() / ".crabcode" / "mcp_servers.local.json",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
for path in paths:
|
|
36
|
+
if not path.exists():
|
|
37
|
+
continue
|
|
38
|
+
try:
|
|
39
|
+
raw = json.loads(path.read_text(encoding="utf-8", errors="replace"))
|
|
40
|
+
if isinstance(raw, dict):
|
|
41
|
+
servers = raw.get("mcpServers", raw)
|
|
42
|
+
if isinstance(servers, dict):
|
|
43
|
+
for name, config in servers.items():
|
|
44
|
+
if isinstance(config, dict):
|
|
45
|
+
merged[name] = {**merged.get(name, {}), **config}
|
|
46
|
+
except (json.JSONDecodeError, OSError):
|
|
47
|
+
logger.warning("Failed to load MCP config file: %s", path, exc_info=True)
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
result: dict[str, McpServerConfig] = {}
|
|
51
|
+
for name, raw in merged.items():
|
|
52
|
+
try:
|
|
53
|
+
result[name] = McpServerConfig.model_validate(raw)
|
|
54
|
+
except Exception:
|
|
55
|
+
logger.warning("Skipping invalid MCP server config: %s", name, exc_info=True)
|
|
56
|
+
|
|
57
|
+
return result
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Validation helpers for portable filesystem path components."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_WINDOWS_INVALID_CHARS = frozenset('<>:"|?*')
|
|
9
|
+
_WINDOWS_RESERVED_NAMES = frozenset(
|
|
10
|
+
{"CON", "PRN", "AUX", "NUL"}
|
|
11
|
+
| {f"COM{number}" for number in range(1, 10)}
|
|
12
|
+
| {f"LPT{number}" for number in range(1, 10)}
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_path_component(value: str, label: str) -> str:
|
|
17
|
+
"""Return a safe, cross-platform filename component or raise ValueError."""
|
|
18
|
+
if not isinstance(value, str) or not value or value in {".", ".."}:
|
|
19
|
+
raise ValueError(f"Invalid {label}")
|
|
20
|
+
if Path(value).is_absolute() or "/" in value or "\\" in value:
|
|
21
|
+
raise ValueError(f"Invalid {label}: path separators are not allowed")
|
|
22
|
+
if any(ord(character) < 32 or ord(character) == 127 for character in value):
|
|
23
|
+
raise ValueError(f"Invalid {label}: control characters are not allowed")
|
|
24
|
+
invalid = sorted(set(value) & _WINDOWS_INVALID_CHARS)
|
|
25
|
+
if invalid:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"Invalid {label}: unsupported filename character(s): {''.join(invalid)}"
|
|
28
|
+
)
|
|
29
|
+
if value.endswith((" ", ".")):
|
|
30
|
+
raise ValueError(f"Invalid {label}: trailing spaces and dots are not allowed")
|
|
31
|
+
stem = value.split(".", 1)[0].rstrip(" .").upper()
|
|
32
|
+
if stem in _WINDOWS_RESERVED_NAMES:
|
|
33
|
+
raise ValueError(f"Invalid {label}: reserved Windows device name")
|
|
34
|
+
return value
|
crabcode_core/paths.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Shared locations for CrabCode's user-level configuration and state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_config_home() -> Path:
|
|
10
|
+
"""Return CRABCODE_HOME, or ~/.crabcode when unset or blank.
|
|
11
|
+
|
|
12
|
+
Require an absolute directory so processes launched in different projects
|
|
13
|
+
share the same home. A leading ``~`` for the current user is supported.
|
|
14
|
+
This function does not create the directory or migrate existing data.
|
|
15
|
+
"""
|
|
16
|
+
override = os.environ.get("CRABCODE_HOME", "").strip()
|
|
17
|
+
if not override:
|
|
18
|
+
return Path.home() / ".crabcode"
|
|
19
|
+
path = Path(override)
|
|
20
|
+
if path.parts and path.parts[0] == "~":
|
|
21
|
+
path = Path.home().joinpath(*path.parts[1:])
|
|
22
|
+
if not path.is_absolute():
|
|
23
|
+
raise ValueError("CRABCODE_HOME must be an absolute directory path (or start with ~/)")
|
|
24
|
+
return path
|