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.
Files changed (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. crabcode_search/tool.py +268 -0
@@ -0,0 +1,211 @@
1
+ """Bounded process isolation for filesystem calls that may never return.
2
+
3
+ Neither process creation nor pipe I/O runs on the event loop or its default
4
+ executor. A daemon supervisor owns each child until it is reaped. Cancelled
5
+ children still consume a slot: a broken mount must not produce unlimited
6
+ processes, threads, or open pipes. SIGKILL is a request, not a successful reap.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ from dataclasses import asdict
13
+ import json
14
+ import math
15
+ import os
16
+ import signal
17
+ import subprocess
18
+ import sys
19
+ import threading
20
+ from typing import Any, Callable
21
+
22
+ from crabcode_core.subprocess_utils import managed_process_command, subprocess_group_options
23
+ from crabcode_core.types.config import DEFAULT_FILESYSTEM_TIMEOUT
24
+
25
+ _SLOTS = threading.BoundedSemaphore(8)
26
+ _ACTIVE_GUARD = threading.Lock()
27
+ _ACTIVE_ABORTS: set[Callable[[], None]] = set()
28
+ IO_TIMEOUT = DEFAULT_FILESYSTEM_TIMEOUT
29
+
30
+ # Do not resolve/stat package paths in the caller. Imports and path resolution
31
+ # happen in the child, including for editable installs on network storage.
32
+ _BOOTSTRAP = """
33
+ import sys
34
+ sys.path[:0] = sys.argv[1:]
35
+ from crabcode_core.io_worker import _worker_main
36
+ _worker_main()
37
+ """
38
+
39
+
40
+ def _kill(process: subprocess.Popen[bytes]) -> None:
41
+ if process.returncode is not None:
42
+ return
43
+ try:
44
+ if os.name == "nt":
45
+ process.kill() # The Windows supervisor owns a kill-on-close job.
46
+ else:
47
+ os.killpg(process.pid, signal.SIGKILL)
48
+ except ProcessLookupError:
49
+ pass
50
+
51
+
52
+ def abort_io_workers() -> None:
53
+ """Request worker termination during emergency exit, without waiting."""
54
+ with _ACTIVE_GUARD:
55
+ callbacks = tuple(_ACTIVE_ABORTS)
56
+ for abort in callbacks:
57
+ abort()
58
+
59
+
60
+ async def run_io(module: str, function: str, *args: Any,
61
+ timeout: float | None = IO_TIMEOUT, **kwargs: Any) -> Any:
62
+ """Call an internal function in an expendable process; None disables the deadline."""
63
+ slots = _SLOTS
64
+ if not slots.acquire(blocking=False):
65
+ raise OSError("Filesystem workers are busy or still blocked; stop retrying until storage recovers")
66
+ loop = asyncio.get_running_loop()
67
+ result = loop.create_future()
68
+ guard = threading.Lock()
69
+ abandoned = False
70
+ process: subprocess.Popen[bytes] | None = None
71
+
72
+ def abort() -> None:
73
+ nonlocal abandoned
74
+ with guard:
75
+ abandoned = True
76
+ child = process
77
+ if child is not None:
78
+ _kill(child)
79
+
80
+ def deliver(value: Any, error: BaseException | None) -> None:
81
+ if not result.done():
82
+ if error is None:
83
+ result.set_result(value)
84
+ else:
85
+ result.set_exception(error)
86
+
87
+ def supervise() -> None:
88
+ nonlocal process
89
+ value, error = None, None
90
+ child = None
91
+ try:
92
+ payload = json.dumps([module, function, args, kwargs, os.getcwd()]).encode("utf-8")
93
+ # Preserve editable-install search roots, but never search the
94
+ # target workspace implicitly via an empty sys.path entry.
95
+ roots = [p for p in sys.path if p and os.path.isabs(p)]
96
+ child = subprocess.Popen(
97
+ managed_process_command([sys.executable, "-I", "-c", _BOOTSTRAP, *roots]),
98
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
99
+ cwd=os.path.abspath(os.sep), **subprocess_group_options(),
100
+ )
101
+ with guard:
102
+ process = child
103
+ cancelled = abandoned
104
+ if cancelled:
105
+ _kill(child)
106
+ payload = None # Never begin an operation after its caller left.
107
+ stdout, stderr = child.communicate(payload)
108
+ if child.returncode:
109
+ raise OSError(f"Filesystem worker failed ({child.returncode}): "
110
+ + stderr.decode("utf-8", errors="replace")[-2000:])
111
+ reply = json.loads(stdout)
112
+ if "error" in reply:
113
+ raise OSError(reply["error"])
114
+ value = reply["result"]
115
+ except BaseException as exc:
116
+ error = exc
117
+ finally:
118
+ # Even exceptional pipe handling must not release a live child slot.
119
+ if child is not None:
120
+ try:
121
+ if child.poll() is None:
122
+ _kill(child)
123
+ child.wait() # Only this daemon thread can wait indefinitely.
124
+ finally:
125
+ for stream in (child.stdin, child.stdout, child.stderr):
126
+ if stream is not None:
127
+ stream.close()
128
+ with _ACTIVE_GUARD:
129
+ _ACTIVE_ABORTS.discard(abort)
130
+ slots.release()
131
+ try:
132
+ loop.call_soon_threadsafe(deliver, value, error)
133
+ except RuntimeError:
134
+ pass # Caller and its event loop have already exited.
135
+
136
+ try:
137
+ with _ACTIVE_GUARD:
138
+ _ACTIVE_ABORTS.add(abort)
139
+ threading.Thread(target=supervise, name="crabcode-fs-worker", daemon=True).start()
140
+ except BaseException:
141
+ with _ACTIVE_GUARD:
142
+ _ACTIVE_ABORTS.discard(abort)
143
+ slots.release()
144
+ raise
145
+ try:
146
+ return await asyncio.wait_for(result, timeout=timeout)
147
+ finally:
148
+ if not result.done() or result.cancelled():
149
+ abort()
150
+
151
+
152
+ async def run_file_tool(name: str, tool_input: dict[str, Any], context: Any) -> Any:
153
+ from crabcode_core.types.tool import ToolResult
154
+
155
+ # All session objects/callbacks stay in the parent. Snapshot metadata is
156
+ # explicit; filesystem changes and their backups execute in the same child.
157
+ fields = {key: getattr(context, key) for key in (
158
+ "cwd", "session_id", "snapshot_enabled", "snapshot_max_size_mb", "env",
159
+ "filesystem_timeout",
160
+ )}
161
+ timeout = context.filesystem_timeout
162
+ if name == "Bash" and "timeout" in tool_input:
163
+ try:
164
+ timeout = float(tool_input["timeout"])
165
+ if not math.isfinite(timeout) or timeout <= 0:
166
+ raise ValueError
167
+ except (ValueError, TypeError):
168
+ return ToolResult(result_for_model="Error: timeout must be a positive number of seconds",
169
+ is_error=True)
170
+ try:
171
+ data = await run_io(__name__, "_call_file_tool", name, tool_input, fields,
172
+ timeout=timeout)
173
+ return ToolResult(**data)
174
+ except (OSError, asyncio.TimeoutError) as exc:
175
+ detail = str(exc) or (
176
+ f"timed out after {timeout:g}s" if timeout is not None else type(exc).__name__
177
+ )
178
+ if name in {"apply_patch", "Edit", "Write", "Bash"}:
179
+ detail += ". Write outcome is unknown; inspect affected files and snapshots before retrying"
180
+ return ToolResult(result_for_model=f"{name} filesystem operation failed: {detail}",
181
+ is_error=True)
182
+
183
+
184
+ async def _call_file_tool(name: str, tool_input: dict[str, Any], fields: dict[str, Any]) -> dict:
185
+ from importlib import import_module
186
+ from crabcode_core.types.tool import ToolContext
187
+
188
+ modules = {"Read": ("file_read", "FileReadTool"), "Write": ("file_write", "FileWriteTool"),
189
+ "apply_patch": ("apply_patch", "ApplyPatchTool"),
190
+ "Edit": ("file_edit", "FileEditTool"), "Glob": ("glob", "GlobTool"),
191
+ "Grep": ("grep", "GrepTool"), "Bash": ("bash", "BashTool"),
192
+ "Image": ("image", "ImageTool")}
193
+ module, cls = modules[name]
194
+ tool = getattr(import_module(f"crabcode_core.tools.{module}"), cls)()
195
+ return asdict(await tool._call_local(tool_input, ToolContext(**fields)))
196
+
197
+
198
+ def _worker_main() -> None:
199
+ import importlib
200
+ import inspect
201
+
202
+ try:
203
+ module, function, args, kwargs, caller_cwd = json.load(sys.stdin)
204
+ os.chdir(caller_cwd)
205
+ value = getattr(importlib.import_module(module), function)(*args, **kwargs)
206
+ if inspect.isawaitable(value):
207
+ value = asyncio.run(value)
208
+ reply = {"result": value}
209
+ except Exception as exc:
210
+ reply = {"error": f"{type(exc).__name__}: {exc}"}
211
+ sys.stdout.write(json.dumps(reply, ensure_ascii=True))
@@ -0,0 +1,168 @@
1
+ """Shared runtime logging configuration for CrabCode."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import copy
8
+ import queue
9
+ import threading
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING
12
+
13
+ from crabcode_core.paths import get_config_home
14
+
15
+ if TYPE_CHECKING:
16
+ from crabcode_core.types.config import LoggingSettings
17
+
18
+
19
+ LOG_NAMESPACE = "crabcode"
20
+ LOG_KEY = "crabcode"
21
+ LOG_FILE_NAME = "crabcode.log"
22
+ LOG_INDEX_NAME = "index.json"
23
+ LOGS_DIR_NAME = ".crabcode/logs"
24
+
25
+ _configured_signature: tuple[str, str, str] | None = None
26
+
27
+
28
+ class _AsyncFileHandler(logging.Handler):
29
+ """Keep disk writes and traceback source lookups off application threads.
30
+
31
+ A stalled sink has bounded memory and a daemon owner. It must never hold
32
+ the logging handler lock or make logging.shutdown wait for remote storage.
33
+ """
34
+
35
+ def __init__(self, path: Path) -> None:
36
+ super().__init__()
37
+ self._path = path
38
+ self._records: queue.Queue[logging.LogRecord | None] = queue.Queue(maxsize=1024)
39
+ self._stopped = threading.Event()
40
+ self._writer = threading.Thread(target=self._write, name="crabcode-log-writer", daemon=True)
41
+ self._writer.start()
42
+
43
+ def emit(self, record: logging.LogRecord) -> None:
44
+ if self._stopped.is_set():
45
+ return
46
+ try:
47
+ # Formatting exceptions can read source files; do it in the writer.
48
+ self._records.put_nowait(copy.copy(record))
49
+ except queue.Full:
50
+ pass
51
+
52
+ def _write(self) -> None:
53
+ try:
54
+ with open(self._path, "a", encoding="utf-8") as stream:
55
+ while True:
56
+ try:
57
+ record = self._records.get(timeout=0.1)
58
+ except queue.Empty:
59
+ if self._stopped.is_set():
60
+ break
61
+ continue
62
+ if record is None:
63
+ break
64
+ stream.write(self.format(record) + "\n")
65
+ stream.flush()
66
+ except Exception:
67
+ # Do not recursively log a failed/full logging sink.
68
+ self._stopped.set()
69
+
70
+ def close(self) -> None:
71
+ self._stopped.set()
72
+ try:
73
+ self._records.put_nowait(None)
74
+ except queue.Full:
75
+ pass
76
+ if threading.current_thread() is not self._writer:
77
+ self._writer.join(timeout=0.2)
78
+ super().close()
79
+
80
+
81
+ def get_logger(name: str | None = None) -> logging.Logger:
82
+ """Return a logger under the shared CrabCode namespace."""
83
+ if not name:
84
+ return logging.getLogger(LOG_NAMESPACE)
85
+ if name.startswith(f"{LOG_NAMESPACE}."):
86
+ return logging.getLogger(name)
87
+ return logging.getLogger(f"{LOG_NAMESPACE}.{name}")
88
+
89
+
90
+ def get_logs_dir(cwd: str) -> Path:
91
+ """Return the per-project logs directory."""
92
+ logs_dir = Path(cwd).resolve() / LOGS_DIR_NAME
93
+ try:
94
+ logs_dir.mkdir(parents=True, exist_ok=True)
95
+ except OSError:
96
+ # cwd may be read-only (e.g. "/" on macOS) — fall back to config home
97
+ logs_dir = get_config_home() / "logs"
98
+ logs_dir.mkdir(parents=True, exist_ok=True)
99
+ return logs_dir
100
+
101
+
102
+ def get_log_path(cwd: str, settings: LoggingSettings | None = None) -> Path:
103
+ """Resolve the main CrabCode log path."""
104
+ if settings and settings.file:
105
+ configured = Path(settings.file)
106
+ if not configured.is_absolute():
107
+ configured = Path(cwd).resolve() / configured
108
+ try:
109
+ configured.parent.mkdir(parents=True, exist_ok=True)
110
+ except OSError:
111
+ configured = get_config_home() / "logs" / configured.name
112
+ configured.parent.mkdir(parents=True, exist_ok=True)
113
+ return configured
114
+ return get_logs_dir(cwd) / LOG_FILE_NAME
115
+
116
+
117
+ def configure_logging(cwd: str, settings: LoggingSettings | None = None) -> Path:
118
+ """Configure the shared CrabCode logger to write to a project log file."""
119
+ global _configured_signature
120
+
121
+ from crabcode_core.types.config import LoggingSettings
122
+
123
+ logging_settings = settings or LoggingSettings()
124
+ level_name = logging_settings.level.upper()
125
+ log_path = get_log_path(cwd, logging_settings).resolve()
126
+ signature = (str(Path(cwd).resolve()), level_name, str(log_path))
127
+ logger = logging.getLogger(LOG_NAMESPACE)
128
+
129
+ if _configured_signature == signature and logger.handlers:
130
+ _register_log(cwd, LOG_KEY, log_path)
131
+ return log_path
132
+
133
+ for old_handler in logger.handlers[:]:
134
+ logger.removeHandler(old_handler)
135
+ old_handler.close()
136
+ logger.setLevel(getattr(logging, level_name, logging.WARNING))
137
+ logger.propagate = False
138
+
139
+ handler = _AsyncFileHandler(log_path)
140
+ handler.setLevel(logger.level)
141
+ handler.setFormatter(
142
+ logging.Formatter(
143
+ fmt="%(asctime)s %(levelname)s %(name)s %(message)s",
144
+ datefmt="%Y-%m-%d %H:%M:%S",
145
+ )
146
+ )
147
+ logger.addHandler(handler)
148
+
149
+ _configured_signature = signature
150
+ _register_log(cwd, LOG_KEY, log_path)
151
+ logger.debug("Logging configured: level=%s path=%s", level_name, log_path)
152
+ return log_path
153
+
154
+
155
+ def _register_log(cwd: str, key: str, log_path: Path) -> None:
156
+ """Register a log file so the CLI /logs command can discover it."""
157
+ index_path = get_logs_dir(cwd) / LOG_INDEX_NAME
158
+ try:
159
+ data = json.loads(index_path.read_text(encoding="utf-8")) if index_path.exists() else {}
160
+ except (OSError, json.JSONDecodeError):
161
+ data = {}
162
+ if not isinstance(data, dict):
163
+ data = {}
164
+ data[key] = str(log_path.resolve())
165
+ index_path.write_text(
166
+ json.dumps(data, ensure_ascii=False, indent=2),
167
+ encoding="utf-8",
168
+ )
@@ -0,0 +1,6 @@
1
+ """LSP (Language Server Protocol) client support."""
2
+
3
+ from crabcode_core.lsp.client import LSPClient
4
+ from crabcode_core.lsp.manager import LSPManager
5
+
6
+ __all__ = ["LSPClient", "LSPManager"]