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.
- llm_code/__init__.py +2 -0
- llm_code/analysis/__init__.py +6 -0
- llm_code/analysis/cache.py +33 -0
- llm_code/analysis/engine.py +256 -0
- llm_code/analysis/go_rules.py +114 -0
- llm_code/analysis/js_rules.py +84 -0
- llm_code/analysis/python_rules.py +311 -0
- llm_code/analysis/rules.py +140 -0
- llm_code/analysis/rust_rules.py +108 -0
- llm_code/analysis/universal_rules.py +111 -0
- llm_code/api/__init__.py +0 -0
- llm_code/api/client.py +90 -0
- llm_code/api/errors.py +73 -0
- llm_code/api/openai_compat.py +390 -0
- llm_code/api/provider.py +35 -0
- llm_code/api/sse.py +52 -0
- llm_code/api/types.py +140 -0
- llm_code/cli/__init__.py +0 -0
- llm_code/cli/commands.py +70 -0
- llm_code/cli/image.py +122 -0
- llm_code/cli/render.py +214 -0
- llm_code/cli/status_line.py +79 -0
- llm_code/cli/streaming.py +92 -0
- llm_code/cli/tui_main.py +220 -0
- llm_code/computer_use/__init__.py +11 -0
- llm_code/computer_use/app_detect.py +49 -0
- llm_code/computer_use/app_tier.py +57 -0
- llm_code/computer_use/coordinator.py +99 -0
- llm_code/computer_use/input_control.py +71 -0
- llm_code/computer_use/screenshot.py +93 -0
- llm_code/cron/__init__.py +13 -0
- llm_code/cron/parser.py +145 -0
- llm_code/cron/scheduler.py +135 -0
- llm_code/cron/storage.py +126 -0
- llm_code/enterprise/__init__.py +1 -0
- llm_code/enterprise/audit.py +59 -0
- llm_code/enterprise/auth.py +26 -0
- llm_code/enterprise/oidc.py +95 -0
- llm_code/enterprise/rbac.py +65 -0
- llm_code/harness/__init__.py +5 -0
- llm_code/harness/config.py +33 -0
- llm_code/harness/engine.py +129 -0
- llm_code/harness/guides.py +41 -0
- llm_code/harness/sensors.py +68 -0
- llm_code/harness/templates.py +84 -0
- llm_code/hida/__init__.py +1 -0
- llm_code/hida/classifier.py +187 -0
- llm_code/hida/engine.py +49 -0
- llm_code/hida/profiles.py +95 -0
- llm_code/hida/types.py +28 -0
- llm_code/ide/__init__.py +1 -0
- llm_code/ide/bridge.py +80 -0
- llm_code/ide/detector.py +76 -0
- llm_code/ide/server.py +169 -0
- llm_code/logging.py +29 -0
- llm_code/lsp/__init__.py +0 -0
- llm_code/lsp/client.py +298 -0
- llm_code/lsp/detector.py +42 -0
- llm_code/lsp/manager.py +56 -0
- llm_code/lsp/tools.py +288 -0
- llm_code/marketplace/__init__.py +0 -0
- llm_code/marketplace/builtin_registry.py +102 -0
- llm_code/marketplace/installer.py +162 -0
- llm_code/marketplace/plugin.py +78 -0
- llm_code/marketplace/registry.py +360 -0
- llm_code/mcp/__init__.py +0 -0
- llm_code/mcp/bridge.py +87 -0
- llm_code/mcp/client.py +117 -0
- llm_code/mcp/health.py +120 -0
- llm_code/mcp/manager.py +214 -0
- llm_code/mcp/oauth.py +219 -0
- llm_code/mcp/transport.py +254 -0
- llm_code/mcp/types.py +53 -0
- llm_code/remote/__init__.py +0 -0
- llm_code/remote/client.py +136 -0
- llm_code/remote/protocol.py +22 -0
- llm_code/remote/server.py +275 -0
- llm_code/remote/ssh_proxy.py +56 -0
- llm_code/runtime/__init__.py +0 -0
- llm_code/runtime/auto_commit.py +56 -0
- llm_code/runtime/auto_diagnose.py +62 -0
- llm_code/runtime/checkpoint.py +70 -0
- llm_code/runtime/checkpoint_recovery.py +142 -0
- llm_code/runtime/compaction.py +35 -0
- llm_code/runtime/compressor.py +415 -0
- llm_code/runtime/config.py +533 -0
- llm_code/runtime/context.py +49 -0
- llm_code/runtime/conversation.py +921 -0
- llm_code/runtime/cost_tracker.py +126 -0
- llm_code/runtime/dream.py +127 -0
- llm_code/runtime/file_protection.py +150 -0
- llm_code/runtime/hardware.py +85 -0
- llm_code/runtime/hooks.py +223 -0
- llm_code/runtime/indexer.py +230 -0
- llm_code/runtime/knowledge_compiler.py +232 -0
- llm_code/runtime/memory.py +132 -0
- llm_code/runtime/memory_layers.py +467 -0
- llm_code/runtime/memory_lint.py +252 -0
- llm_code/runtime/model_aliases.py +37 -0
- llm_code/runtime/ollama.py +93 -0
- llm_code/runtime/overlay.py +124 -0
- llm_code/runtime/permissions.py +200 -0
- llm_code/runtime/plan.py +45 -0
- llm_code/runtime/prompt.py +238 -0
- llm_code/runtime/repo_map.py +174 -0
- llm_code/runtime/sandbox.py +116 -0
- llm_code/runtime/session.py +268 -0
- llm_code/runtime/skill_resolver.py +61 -0
- llm_code/runtime/skills.py +133 -0
- llm_code/runtime/speculative.py +75 -0
- llm_code/runtime/streaming_executor.py +216 -0
- llm_code/runtime/telemetry.py +196 -0
- llm_code/runtime/token_budget.py +26 -0
- llm_code/runtime/vcr.py +142 -0
- llm_code/runtime/vision.py +102 -0
- llm_code/swarm/__init__.py +1 -0
- llm_code/swarm/backend_subprocess.py +108 -0
- llm_code/swarm/backend_tmux.py +103 -0
- llm_code/swarm/backend_worktree.py +306 -0
- llm_code/swarm/checkpoint.py +74 -0
- llm_code/swarm/coordinator.py +236 -0
- llm_code/swarm/mailbox.py +88 -0
- llm_code/swarm/manager.py +202 -0
- llm_code/swarm/memory_sync.py +80 -0
- llm_code/swarm/recovery.py +21 -0
- llm_code/swarm/team.py +67 -0
- llm_code/swarm/types.py +31 -0
- llm_code/task/__init__.py +16 -0
- llm_code/task/diagnostics.py +93 -0
- llm_code/task/manager.py +162 -0
- llm_code/task/types.py +112 -0
- llm_code/task/verifier.py +104 -0
- llm_code/tools/__init__.py +0 -0
- llm_code/tools/agent.py +145 -0
- llm_code/tools/agent_roles.py +82 -0
- llm_code/tools/base.py +94 -0
- llm_code/tools/bash.py +565 -0
- llm_code/tools/computer_use_tools.py +278 -0
- llm_code/tools/coordinator_tool.py +75 -0
- llm_code/tools/cron_create.py +90 -0
- llm_code/tools/cron_delete.py +49 -0
- llm_code/tools/cron_list.py +51 -0
- llm_code/tools/deferred.py +92 -0
- llm_code/tools/dump.py +116 -0
- llm_code/tools/edit_file.py +282 -0
- llm_code/tools/git_tools.py +531 -0
- llm_code/tools/glob_search.py +112 -0
- llm_code/tools/grep_search.py +144 -0
- llm_code/tools/ide_diagnostics.py +59 -0
- llm_code/tools/ide_open.py +58 -0
- llm_code/tools/ide_selection.py +52 -0
- llm_code/tools/memory_tools.py +138 -0
- llm_code/tools/multi_edit.py +143 -0
- llm_code/tools/notebook_edit.py +107 -0
- llm_code/tools/notebook_read.py +81 -0
- llm_code/tools/parsing.py +63 -0
- llm_code/tools/read_file.py +154 -0
- llm_code/tools/registry.py +58 -0
- llm_code/tools/search_backends/__init__.py +56 -0
- llm_code/tools/search_backends/brave.py +56 -0
- llm_code/tools/search_backends/duckduckgo.py +129 -0
- llm_code/tools/search_backends/searxng.py +71 -0
- llm_code/tools/search_backends/tavily.py +73 -0
- llm_code/tools/swarm_create.py +109 -0
- llm_code/tools/swarm_delete.py +95 -0
- llm_code/tools/swarm_list.py +44 -0
- llm_code/tools/swarm_message.py +109 -0
- llm_code/tools/task_close.py +79 -0
- llm_code/tools/task_plan.py +79 -0
- llm_code/tools/task_verify.py +90 -0
- llm_code/tools/tool_search.py +65 -0
- llm_code/tools/web_common.py +258 -0
- llm_code/tools/web_fetch.py +223 -0
- llm_code/tools/web_search.py +280 -0
- llm_code/tools/write_file.py +118 -0
- llm_code/tui/__init__.py +1 -0
- llm_code/tui/app.py +2432 -0
- llm_code/tui/chat_view.py +82 -0
- llm_code/tui/chat_widgets.py +309 -0
- llm_code/tui/header_bar.py +46 -0
- llm_code/tui/input_bar.py +349 -0
- llm_code/tui/keybindings.py +142 -0
- llm_code/tui/marketplace.py +210 -0
- llm_code/tui/status_bar.py +72 -0
- llm_code/tui/theme.py +96 -0
- llm_code/utils/__init__.py +0 -0
- llm_code/utils/diff.py +111 -0
- llm_code/utils/errors.py +70 -0
- llm_code/utils/hyperlink.py +73 -0
- llm_code/utils/notebook.py +179 -0
- llm_code/utils/search.py +69 -0
- llm_code/utils/text_normalize.py +28 -0
- llm_code/utils/version_check.py +62 -0
- llm_code/vim/__init__.py +4 -0
- llm_code/vim/engine.py +51 -0
- llm_code/vim/motions.py +172 -0
- llm_code/vim/operators.py +183 -0
- llm_code/vim/text_objects.py +139 -0
- llm_code/vim/transitions.py +279 -0
- llm_code/vim/types.py +68 -0
- llm_code/voice/__init__.py +1 -0
- llm_code/voice/languages.py +43 -0
- llm_code/voice/recorder.py +136 -0
- llm_code/voice/stt.py +36 -0
- llm_code/voice/stt_anthropic.py +66 -0
- llm_code/voice/stt_google.py +32 -0
- llm_code/voice/stt_whisper.py +52 -0
- llmcode_cli-1.0.0.dist-info/METADATA +524 -0
- llmcode_cli-1.0.0.dist-info/RECORD +212 -0
- llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
- llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|
llm_code/ide/__init__.py
ADDED
|
@@ -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
|
llm_code/ide/detector.py
ADDED
|
@@ -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}")
|
llm_code/lsp/__init__.py
ADDED
|
File without changes
|