llmcode-cli 1.0.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.
Files changed (212) hide show
  1. llm_code/__init__.py +2 -0
  2. llm_code/analysis/__init__.py +6 -0
  3. llm_code/analysis/cache.py +33 -0
  4. llm_code/analysis/engine.py +256 -0
  5. llm_code/analysis/go_rules.py +114 -0
  6. llm_code/analysis/js_rules.py +84 -0
  7. llm_code/analysis/python_rules.py +311 -0
  8. llm_code/analysis/rules.py +140 -0
  9. llm_code/analysis/rust_rules.py +108 -0
  10. llm_code/analysis/universal_rules.py +111 -0
  11. llm_code/api/__init__.py +0 -0
  12. llm_code/api/client.py +90 -0
  13. llm_code/api/errors.py +73 -0
  14. llm_code/api/openai_compat.py +390 -0
  15. llm_code/api/provider.py +35 -0
  16. llm_code/api/sse.py +52 -0
  17. llm_code/api/types.py +140 -0
  18. llm_code/cli/__init__.py +0 -0
  19. llm_code/cli/commands.py +70 -0
  20. llm_code/cli/image.py +122 -0
  21. llm_code/cli/render.py +214 -0
  22. llm_code/cli/status_line.py +79 -0
  23. llm_code/cli/streaming.py +92 -0
  24. llm_code/cli/tui_main.py +220 -0
  25. llm_code/computer_use/__init__.py +11 -0
  26. llm_code/computer_use/app_detect.py +49 -0
  27. llm_code/computer_use/app_tier.py +57 -0
  28. llm_code/computer_use/coordinator.py +99 -0
  29. llm_code/computer_use/input_control.py +71 -0
  30. llm_code/computer_use/screenshot.py +93 -0
  31. llm_code/cron/__init__.py +13 -0
  32. llm_code/cron/parser.py +145 -0
  33. llm_code/cron/scheduler.py +135 -0
  34. llm_code/cron/storage.py +126 -0
  35. llm_code/enterprise/__init__.py +1 -0
  36. llm_code/enterprise/audit.py +59 -0
  37. llm_code/enterprise/auth.py +26 -0
  38. llm_code/enterprise/oidc.py +95 -0
  39. llm_code/enterprise/rbac.py +65 -0
  40. llm_code/harness/__init__.py +5 -0
  41. llm_code/harness/config.py +33 -0
  42. llm_code/harness/engine.py +129 -0
  43. llm_code/harness/guides.py +41 -0
  44. llm_code/harness/sensors.py +68 -0
  45. llm_code/harness/templates.py +84 -0
  46. llm_code/hida/__init__.py +1 -0
  47. llm_code/hida/classifier.py +187 -0
  48. llm_code/hida/engine.py +49 -0
  49. llm_code/hida/profiles.py +95 -0
  50. llm_code/hida/types.py +28 -0
  51. llm_code/ide/__init__.py +1 -0
  52. llm_code/ide/bridge.py +80 -0
  53. llm_code/ide/detector.py +76 -0
  54. llm_code/ide/server.py +169 -0
  55. llm_code/logging.py +29 -0
  56. llm_code/lsp/__init__.py +0 -0
  57. llm_code/lsp/client.py +298 -0
  58. llm_code/lsp/detector.py +42 -0
  59. llm_code/lsp/manager.py +56 -0
  60. llm_code/lsp/tools.py +288 -0
  61. llm_code/marketplace/__init__.py +0 -0
  62. llm_code/marketplace/builtin_registry.py +102 -0
  63. llm_code/marketplace/installer.py +162 -0
  64. llm_code/marketplace/plugin.py +78 -0
  65. llm_code/marketplace/registry.py +360 -0
  66. llm_code/mcp/__init__.py +0 -0
  67. llm_code/mcp/bridge.py +87 -0
  68. llm_code/mcp/client.py +117 -0
  69. llm_code/mcp/health.py +120 -0
  70. llm_code/mcp/manager.py +214 -0
  71. llm_code/mcp/oauth.py +219 -0
  72. llm_code/mcp/transport.py +254 -0
  73. llm_code/mcp/types.py +53 -0
  74. llm_code/remote/__init__.py +0 -0
  75. llm_code/remote/client.py +136 -0
  76. llm_code/remote/protocol.py +22 -0
  77. llm_code/remote/server.py +275 -0
  78. llm_code/remote/ssh_proxy.py +56 -0
  79. llm_code/runtime/__init__.py +0 -0
  80. llm_code/runtime/auto_commit.py +56 -0
  81. llm_code/runtime/auto_diagnose.py +62 -0
  82. llm_code/runtime/checkpoint.py +70 -0
  83. llm_code/runtime/checkpoint_recovery.py +142 -0
  84. llm_code/runtime/compaction.py +35 -0
  85. llm_code/runtime/compressor.py +415 -0
  86. llm_code/runtime/config.py +533 -0
  87. llm_code/runtime/context.py +49 -0
  88. llm_code/runtime/conversation.py +921 -0
  89. llm_code/runtime/cost_tracker.py +126 -0
  90. llm_code/runtime/dream.py +127 -0
  91. llm_code/runtime/file_protection.py +150 -0
  92. llm_code/runtime/hardware.py +85 -0
  93. llm_code/runtime/hooks.py +223 -0
  94. llm_code/runtime/indexer.py +230 -0
  95. llm_code/runtime/knowledge_compiler.py +232 -0
  96. llm_code/runtime/memory.py +132 -0
  97. llm_code/runtime/memory_layers.py +467 -0
  98. llm_code/runtime/memory_lint.py +252 -0
  99. llm_code/runtime/model_aliases.py +37 -0
  100. llm_code/runtime/ollama.py +93 -0
  101. llm_code/runtime/overlay.py +124 -0
  102. llm_code/runtime/permissions.py +200 -0
  103. llm_code/runtime/plan.py +45 -0
  104. llm_code/runtime/prompt.py +238 -0
  105. llm_code/runtime/repo_map.py +174 -0
  106. llm_code/runtime/sandbox.py +116 -0
  107. llm_code/runtime/session.py +268 -0
  108. llm_code/runtime/skill_resolver.py +61 -0
  109. llm_code/runtime/skills.py +133 -0
  110. llm_code/runtime/speculative.py +75 -0
  111. llm_code/runtime/streaming_executor.py +216 -0
  112. llm_code/runtime/telemetry.py +196 -0
  113. llm_code/runtime/token_budget.py +26 -0
  114. llm_code/runtime/vcr.py +142 -0
  115. llm_code/runtime/vision.py +102 -0
  116. llm_code/swarm/__init__.py +1 -0
  117. llm_code/swarm/backend_subprocess.py +108 -0
  118. llm_code/swarm/backend_tmux.py +103 -0
  119. llm_code/swarm/backend_worktree.py +306 -0
  120. llm_code/swarm/checkpoint.py +74 -0
  121. llm_code/swarm/coordinator.py +236 -0
  122. llm_code/swarm/mailbox.py +88 -0
  123. llm_code/swarm/manager.py +202 -0
  124. llm_code/swarm/memory_sync.py +80 -0
  125. llm_code/swarm/recovery.py +21 -0
  126. llm_code/swarm/team.py +67 -0
  127. llm_code/swarm/types.py +31 -0
  128. llm_code/task/__init__.py +16 -0
  129. llm_code/task/diagnostics.py +93 -0
  130. llm_code/task/manager.py +162 -0
  131. llm_code/task/types.py +112 -0
  132. llm_code/task/verifier.py +104 -0
  133. llm_code/tools/__init__.py +0 -0
  134. llm_code/tools/agent.py +145 -0
  135. llm_code/tools/agent_roles.py +82 -0
  136. llm_code/tools/base.py +94 -0
  137. llm_code/tools/bash.py +565 -0
  138. llm_code/tools/computer_use_tools.py +278 -0
  139. llm_code/tools/coordinator_tool.py +75 -0
  140. llm_code/tools/cron_create.py +90 -0
  141. llm_code/tools/cron_delete.py +49 -0
  142. llm_code/tools/cron_list.py +51 -0
  143. llm_code/tools/deferred.py +92 -0
  144. llm_code/tools/dump.py +116 -0
  145. llm_code/tools/edit_file.py +282 -0
  146. llm_code/tools/git_tools.py +531 -0
  147. llm_code/tools/glob_search.py +112 -0
  148. llm_code/tools/grep_search.py +144 -0
  149. llm_code/tools/ide_diagnostics.py +59 -0
  150. llm_code/tools/ide_open.py +58 -0
  151. llm_code/tools/ide_selection.py +52 -0
  152. llm_code/tools/memory_tools.py +138 -0
  153. llm_code/tools/multi_edit.py +143 -0
  154. llm_code/tools/notebook_edit.py +107 -0
  155. llm_code/tools/notebook_read.py +81 -0
  156. llm_code/tools/parsing.py +63 -0
  157. llm_code/tools/read_file.py +154 -0
  158. llm_code/tools/registry.py +58 -0
  159. llm_code/tools/search_backends/__init__.py +56 -0
  160. llm_code/tools/search_backends/brave.py +56 -0
  161. llm_code/tools/search_backends/duckduckgo.py +129 -0
  162. llm_code/tools/search_backends/searxng.py +71 -0
  163. llm_code/tools/search_backends/tavily.py +73 -0
  164. llm_code/tools/swarm_create.py +109 -0
  165. llm_code/tools/swarm_delete.py +95 -0
  166. llm_code/tools/swarm_list.py +44 -0
  167. llm_code/tools/swarm_message.py +109 -0
  168. llm_code/tools/task_close.py +79 -0
  169. llm_code/tools/task_plan.py +79 -0
  170. llm_code/tools/task_verify.py +90 -0
  171. llm_code/tools/tool_search.py +65 -0
  172. llm_code/tools/web_common.py +258 -0
  173. llm_code/tools/web_fetch.py +223 -0
  174. llm_code/tools/web_search.py +280 -0
  175. llm_code/tools/write_file.py +118 -0
  176. llm_code/tui/__init__.py +1 -0
  177. llm_code/tui/app.py +2432 -0
  178. llm_code/tui/chat_view.py +82 -0
  179. llm_code/tui/chat_widgets.py +309 -0
  180. llm_code/tui/header_bar.py +46 -0
  181. llm_code/tui/input_bar.py +349 -0
  182. llm_code/tui/keybindings.py +142 -0
  183. llm_code/tui/marketplace.py +210 -0
  184. llm_code/tui/status_bar.py +72 -0
  185. llm_code/tui/theme.py +96 -0
  186. llm_code/utils/__init__.py +0 -0
  187. llm_code/utils/diff.py +111 -0
  188. llm_code/utils/errors.py +70 -0
  189. llm_code/utils/hyperlink.py +73 -0
  190. llm_code/utils/notebook.py +179 -0
  191. llm_code/utils/search.py +69 -0
  192. llm_code/utils/text_normalize.py +28 -0
  193. llm_code/utils/version_check.py +62 -0
  194. llm_code/vim/__init__.py +4 -0
  195. llm_code/vim/engine.py +51 -0
  196. llm_code/vim/motions.py +172 -0
  197. llm_code/vim/operators.py +183 -0
  198. llm_code/vim/text_objects.py +139 -0
  199. llm_code/vim/transitions.py +279 -0
  200. llm_code/vim/types.py +68 -0
  201. llm_code/voice/__init__.py +1 -0
  202. llm_code/voice/languages.py +43 -0
  203. llm_code/voice/recorder.py +136 -0
  204. llm_code/voice/stt.py +36 -0
  205. llm_code/voice/stt_anthropic.py +66 -0
  206. llm_code/voice/stt_google.py +32 -0
  207. llm_code/voice/stt_whisper.py +52 -0
  208. llmcode_cli-1.0.0.dist-info/METADATA +524 -0
  209. llmcode_cli-1.0.0.dist-info/RECORD +212 -0
  210. llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
  211. llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
  212. llmcode_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,95 @@
1
+ """Default task profiles mapping each TaskType to its tool/memory/governance set."""
2
+ from __future__ import annotations
3
+
4
+ from llm_code.hida.types import TaskProfile, TaskType
5
+
6
+ # Core tool sets shared across profiles
7
+ _FILE_READ = frozenset({"read_file", "glob_search", "grep_search"})
8
+ _FILE_WRITE = frozenset({"write_file", "edit_file"})
9
+ _SHELL = frozenset({"bash"})
10
+ _MEMORY = frozenset({"memory_store", "memory_recall", "memory_list"})
11
+ _GIT = frozenset({"git_diff", "git_log", "git_status"})
12
+ _AGENT = frozenset({"agent"})
13
+
14
+ DEFAULT_PROFILES: dict[TaskType, TaskProfile] = {
15
+ TaskType.CODING: TaskProfile(
16
+ task_type=TaskType.CODING,
17
+ confidence=1.0,
18
+ tools=_FILE_READ | _FILE_WRITE | _SHELL | _AGENT,
19
+ memory_keys=frozenset({"project_stack", "coding_style", "architecture"}),
20
+ governance_categories=frozenset({"coding"}),
21
+ load_full_prompt=False,
22
+ ),
23
+ TaskType.DEBUGGING: TaskProfile(
24
+ task_type=TaskType.DEBUGGING,
25
+ confidence=1.0,
26
+ tools=_FILE_READ | _SHELL | _AGENT,
27
+ memory_keys=frozenset({"known_issues", "project_stack"}),
28
+ governance_categories=frozenset({"debugging"}),
29
+ load_full_prompt=False,
30
+ ),
31
+ TaskType.REVIEWING: TaskProfile(
32
+ task_type=TaskType.REVIEWING,
33
+ confidence=1.0,
34
+ tools=_FILE_READ | _GIT,
35
+ memory_keys=frozenset({"coding_style", "review_guidelines"}),
36
+ governance_categories=frozenset({"reviewing"}),
37
+ load_full_prompt=False,
38
+ ),
39
+ TaskType.PLANNING: TaskProfile(
40
+ task_type=TaskType.PLANNING,
41
+ confidence=1.0,
42
+ tools=_FILE_READ | _MEMORY | _AGENT,
43
+ memory_keys=frozenset({"architecture", "project_stack", "roadmap"}),
44
+ governance_categories=frozenset({"planning"}),
45
+ load_full_prompt=False,
46
+ ),
47
+ TaskType.TESTING: TaskProfile(
48
+ task_type=TaskType.TESTING,
49
+ confidence=1.0,
50
+ tools=_FILE_READ | _FILE_WRITE | _SHELL,
51
+ memory_keys=frozenset({"project_stack", "test_patterns"}),
52
+ governance_categories=frozenset({"testing"}),
53
+ load_full_prompt=False,
54
+ ),
55
+ TaskType.REFACTORING: TaskProfile(
56
+ task_type=TaskType.REFACTORING,
57
+ confidence=1.0,
58
+ tools=_FILE_READ | _FILE_WRITE | _SHELL | _GIT,
59
+ memory_keys=frozenset({"architecture", "coding_style"}),
60
+ governance_categories=frozenset({"refactoring"}),
61
+ load_full_prompt=False,
62
+ ),
63
+ TaskType.RESEARCH: TaskProfile(
64
+ task_type=TaskType.RESEARCH,
65
+ confidence=1.0,
66
+ tools=_FILE_READ | _SHELL | _MEMORY | _AGENT,
67
+ memory_keys=frozenset({"project_stack"}),
68
+ governance_categories=frozenset({"research"}),
69
+ load_full_prompt=False,
70
+ ),
71
+ TaskType.DEPLOYMENT: TaskProfile(
72
+ task_type=TaskType.DEPLOYMENT,
73
+ confidence=1.0,
74
+ tools=_FILE_READ | _FILE_WRITE | _SHELL | _GIT,
75
+ memory_keys=frozenset({"deployment_config", "infrastructure"}),
76
+ governance_categories=frozenset({"deployment"}),
77
+ load_full_prompt=False,
78
+ ),
79
+ TaskType.DOCUMENTATION: TaskProfile(
80
+ task_type=TaskType.DOCUMENTATION,
81
+ confidence=1.0,
82
+ tools=_FILE_READ | _FILE_WRITE | _MEMORY,
83
+ memory_keys=frozenset({"project_stack", "architecture"}),
84
+ governance_categories=frozenset({"documentation"}),
85
+ load_full_prompt=False,
86
+ ),
87
+ TaskType.CONVERSATION: TaskProfile(
88
+ task_type=TaskType.CONVERSATION,
89
+ confidence=1.0,
90
+ tools=_MEMORY,
91
+ memory_keys=frozenset(),
92
+ governance_categories=frozenset({"conversation"}),
93
+ load_full_prompt=False,
94
+ ),
95
+ }
llm_code/hida/types.py ADDED
@@ -0,0 +1,28 @@
1
+ """Frozen dataclasses and enums for HIDA task classification."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+
7
+
8
+ class TaskType(Enum):
9
+ CODING = "coding"
10
+ DEBUGGING = "debugging"
11
+ REVIEWING = "reviewing"
12
+ PLANNING = "planning"
13
+ TESTING = "testing"
14
+ REFACTORING = "refactoring"
15
+ RESEARCH = "research"
16
+ DEPLOYMENT = "deployment"
17
+ DOCUMENTATION = "documentation"
18
+ CONVERSATION = "conversation"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class TaskProfile:
23
+ task_type: TaskType
24
+ confidence: float
25
+ tools: frozenset[str]
26
+ memory_keys: frozenset[str]
27
+ governance_categories: frozenset[str]
28
+ load_full_prompt: bool
@@ -0,0 +1 @@
1
+ """IDE integration package."""
llm_code/ide/bridge.py ADDED
@@ -0,0 +1,80 @@
1
+ """High-level IDE bridge API with graceful fallback."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import Any
6
+
7
+ from llm_code.runtime.config import IDEConfig
8
+ from llm_code.ide.server import IDEServer, JsonRpcError
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class IDEBridge:
14
+ """High-level API for IDE communication. Degrades silently when disconnected."""
15
+
16
+ def __init__(self, config: IDEConfig) -> None:
17
+ self._config = config
18
+ self._server: IDEServer | None = None
19
+
20
+ @property
21
+ def is_enabled(self) -> bool:
22
+ return self._config.enabled
23
+
24
+ @property
25
+ def is_connected(self) -> bool:
26
+ if self._server is None:
27
+ return False
28
+ return self._server.is_running and len(self._server.connected_ides) > 0
29
+
30
+ async def start(self) -> None:
31
+ """Start the WebSocket server if IDE integration is enabled."""
32
+ if not self._config.enabled:
33
+ return
34
+ self._server = IDEServer(port=self._config.port)
35
+ await self._server.start()
36
+ logger.info("IDE bridge listening on port %d", self._server.actual_port)
37
+
38
+ async def stop(self) -> None:
39
+ """Stop the WebSocket server."""
40
+ if self._server is not None:
41
+ await self._server.stop()
42
+ self._server = None
43
+
44
+ async def open_file(self, path: str, line: int | None = None) -> bool:
45
+ """Ask the IDE to open a file. Returns False on failure."""
46
+ params: dict[str, Any] = {"path": path}
47
+ if line is not None:
48
+ params["line"] = line
49
+ result = await self._safe_request("ide/openFile", params)
50
+ return result is not None and result.get("ok", False)
51
+
52
+ async def get_diagnostics(self, path: str) -> list[dict]:
53
+ """Get diagnostics for a file from the IDE. Returns [] on failure."""
54
+ result = await self._safe_request("ide/diagnostics", {"path": path})
55
+ if result is None:
56
+ return []
57
+ return result.get("diagnostics", [])
58
+
59
+ async def get_selection(self) -> dict | None:
60
+ """Get the current editor selection. Returns None on failure."""
61
+ return await self._safe_request("ide/selection", {})
62
+
63
+ async def show_diff(self, path: str, old_text: str, new_text: str) -> bool:
64
+ """Ask the IDE to show a diff. Returns False on failure."""
65
+ result = await self._safe_request("ide/showDiff", {
66
+ "path": path,
67
+ "old_text": old_text,
68
+ "new_text": new_text,
69
+ })
70
+ return result is not None and result.get("ok", False)
71
+
72
+ async def _safe_request(self, method: str, params: dict) -> dict | None:
73
+ """Send a request, returning None on any failure."""
74
+ if self._server is None or not self._server.is_running:
75
+ return None
76
+ try:
77
+ return await self._server.send_request(method, params)
78
+ except (JsonRpcError, OSError, Exception) as exc:
79
+ logger.debug("IDE request %s failed: %s", method, exc)
80
+ return None
@@ -0,0 +1,76 @@
1
+ """Detect running IDE processes."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class IDEInfo:
9
+ name: str
10
+ pid: int
11
+ workspace_path: str
12
+
13
+
14
+ # Process name patterns -> IDE name
15
+ _IDE_PATTERNS: dict[str, str] = {
16
+ "code": "vscode",
17
+ "code-insiders": "vscode",
18
+ "cursor": "vscode",
19
+ "nvim": "neovim",
20
+ "neovim": "neovim",
21
+ "idea": "jetbrains",
22
+ "pycharm": "jetbrains",
23
+ "webstorm": "jetbrains",
24
+ "goland": "jetbrains",
25
+ "clion": "jetbrains",
26
+ "rubymine": "jetbrains",
27
+ "rider": "jetbrains",
28
+ "phpstorm": "jetbrains",
29
+ "datagrip": "jetbrains",
30
+ "subl": "sublime",
31
+ "sublime_text": "sublime",
32
+ }
33
+
34
+
35
+ def _iter_processes() -> list:
36
+ """Iterate over running processes. Requires psutil."""
37
+ import psutil # optional dependency
38
+ return list(psutil.process_iter(["name", "cmdline"]))
39
+
40
+
41
+ def _extract_workspace(cmdline: list[str]) -> str:
42
+ """Best-effort extraction of workspace path from command line args."""
43
+ for arg in reversed(cmdline):
44
+ if arg.startswith("/") and not arg.startswith("--"):
45
+ return arg
46
+ return ""
47
+
48
+
49
+ def detect_running_ide() -> list[IDEInfo]:
50
+ """Scan process list for known IDEs. Returns empty list on failure."""
51
+ try:
52
+ procs = _iter_processes()
53
+ except (ImportError, OSError):
54
+ return []
55
+
56
+ results: list[IDEInfo] = []
57
+ for proc in procs:
58
+ try:
59
+ info = proc.info
60
+ name = (info.get("name") or "").lower()
61
+ cmdline = info.get("cmdline") or []
62
+ except (AttributeError, KeyError):
63
+ continue
64
+
65
+ ide_name = _IDE_PATTERNS.get(name)
66
+ if ide_name is None:
67
+ continue
68
+
69
+ workspace = _extract_workspace(cmdline)
70
+ results.append(IDEInfo(
71
+ name=ide_name,
72
+ pid=proc.pid,
73
+ workspace_path=workspace,
74
+ ))
75
+
76
+ return results
llm_code/ide/server.py ADDED
@@ -0,0 +1,169 @@
1
+ """WebSocket JSON-RPC server for IDE communication."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import json
6
+ import logging
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ _DEFAULT_PORT = 9876
13
+
14
+
15
+ class JsonRpcError(Exception):
16
+ """Raised when a JSON-RPC operation fails."""
17
+
18
+ def __init__(self, code: int, message: str) -> None:
19
+ super().__init__(message)
20
+ self.code = code
21
+
22
+
23
+ @dataclass
24
+ class _ConnectedIDE:
25
+ name: str
26
+ pid: int
27
+ workspace_path: str
28
+ websocket: Any # websockets.WebSocketServerProtocol
29
+
30
+
31
+ class IDEServer:
32
+ """WebSocket JSON-RPC server that IDE extensions connect to."""
33
+
34
+ def __init__(self, port: int = _DEFAULT_PORT) -> None:
35
+ self._port = port
36
+ self._server: Any | None = None
37
+ self._ides: list[_ConnectedIDE] = []
38
+ self._pending: dict[int, asyncio.Future] = {}
39
+ self._next_id = 1000
40
+ self._actual_port: int | None = None
41
+
42
+ @property
43
+ def is_running(self) -> bool:
44
+ return self._server is not None
45
+
46
+ @property
47
+ def actual_port(self) -> int:
48
+ if self._actual_port is None:
49
+ raise RuntimeError("Server not started")
50
+ return self._actual_port
51
+
52
+ @property
53
+ def connected_ides(self) -> list[_ConnectedIDE]:
54
+ return list(self._ides)
55
+
56
+ async def start(self) -> None:
57
+ import websockets
58
+
59
+ self._server = await websockets.serve(
60
+ self._handle_connection,
61
+ "127.0.0.1",
62
+ self._port,
63
+ )
64
+ # Resolve actual port (important when port=0)
65
+ for sock in self._server.sockets:
66
+ addr = sock.getsockname()
67
+ self._actual_port = addr[1]
68
+ break
69
+
70
+ async def stop(self) -> None:
71
+ if self._server is not None:
72
+ self._server.close()
73
+ await self._server.wait_closed()
74
+ self._server = None
75
+ self._ides.clear()
76
+ for fut in self._pending.values():
77
+ if not fut.done():
78
+ fut.set_exception(JsonRpcError(-32000, "Server shutting down"))
79
+ self._pending.clear()
80
+
81
+ async def send_request(self, method: str, params: dict) -> dict:
82
+ """Send a JSON-RPC request to the first connected IDE."""
83
+ if not self._ides:
84
+ raise JsonRpcError(-32000, "No IDE connected")
85
+
86
+ ide = self._ides[-1] # most recently registered
87
+ req_id = self._next_id
88
+ self._next_id += 1
89
+
90
+ msg = json.dumps({
91
+ "jsonrpc": "2.0",
92
+ "method": method,
93
+ "params": params,
94
+ "id": req_id,
95
+ })
96
+
97
+ fut: asyncio.Future[dict] = asyncio.get_event_loop().create_future()
98
+ self._pending[req_id] = fut
99
+
100
+ try:
101
+ await ide.websocket.send(msg)
102
+ return await asyncio.wait_for(fut, timeout=10.0)
103
+ except asyncio.TimeoutError:
104
+ self._pending.pop(req_id, None)
105
+ raise JsonRpcError(-32000, f"IDE did not respond to {method} within 10s")
106
+
107
+ async def _handle_connection(self, websocket: Any) -> None:
108
+ """Handle a single IDE WebSocket connection."""
109
+ ide_entry: _ConnectedIDE | None = None
110
+ try:
111
+ async for raw in websocket:
112
+ try:
113
+ msg = json.loads(raw)
114
+ except json.JSONDecodeError:
115
+ await self._send_error(websocket, None, -32700, "Parse error")
116
+ continue
117
+
118
+ msg_id = msg.get("id")
119
+ method = msg.get("method")
120
+
121
+ # If this is a response to our request (no method field)
122
+ if method is None and msg_id is not None:
123
+ fut = self._pending.pop(msg_id, None)
124
+ if fut is not None and not fut.done():
125
+ if "error" in msg:
126
+ fut.set_exception(JsonRpcError(
127
+ msg["error"].get("code", -32000),
128
+ msg["error"].get("message", "Unknown error"),
129
+ ))
130
+ else:
131
+ fut.set_result(msg.get("result", {}))
132
+ continue
133
+
134
+ # Handle incoming methods from IDE
135
+ if method == "ide/register":
136
+ params = msg.get("params", {})
137
+ ide_entry = _ConnectedIDE(
138
+ name=params.get("name", "unknown"),
139
+ pid=params.get("pid", 0),
140
+ workspace_path=params.get("workspace_path", ""),
141
+ websocket=websocket,
142
+ )
143
+ self._ides.append(ide_entry)
144
+ await self._send_result(websocket, msg_id, {"ok": True})
145
+ logger.info("IDE registered: %s (pid=%d)", ide_entry.name, ide_entry.pid)
146
+ else:
147
+ await self._send_error(websocket, msg_id, -32601, f"Method not found: {method}")
148
+
149
+ except Exception:
150
+ logger.debug("IDE connection closed", exc_info=True)
151
+ finally:
152
+ if ide_entry is not None and ide_entry in self._ides:
153
+ self._ides.remove(ide_entry)
154
+
155
+ @staticmethod
156
+ async def _send_result(websocket: Any, msg_id: int | None, result: dict) -> None:
157
+ await websocket.send(json.dumps({
158
+ "jsonrpc": "2.0",
159
+ "result": result,
160
+ "id": msg_id,
161
+ }))
162
+
163
+ @staticmethod
164
+ async def _send_error(websocket: Any, msg_id: int | None, code: int, message: str) -> None:
165
+ await websocket.send(json.dumps({
166
+ "jsonrpc": "2.0",
167
+ "error": {"code": code, "message": message},
168
+ "id": msg_id,
169
+ }))
llm_code/logging.py ADDED
@@ -0,0 +1,29 @@
1
+ """Structured logging for llm-code."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import sys
6
+
7
+
8
+ def setup_logging(verbose: bool = False) -> logging.Logger:
9
+ """Configure root llm_code logger. Safe to call multiple times."""
10
+ logger = logging.getLogger("llm_code")
11
+ if logger.handlers:
12
+ return logger
13
+
14
+ level = logging.DEBUG if verbose else logging.WARNING
15
+ handler = logging.StreamHandler(sys.stderr)
16
+ handler.setFormatter(
17
+ logging.Formatter(
18
+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
19
+ datefmt="%H:%M:%S",
20
+ )
21
+ )
22
+ logger.addHandler(handler)
23
+ logger.setLevel(level)
24
+ return logger
25
+
26
+
27
+ def get_logger(name: str) -> logging.Logger:
28
+ """Return a child logger under the llm_code namespace."""
29
+ return logging.getLogger(f"llm_code.{name}")
File without changes