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,144 @@
|
|
|
1
|
+
"""GrepSearchTool — regex search across files."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pathlib
|
|
5
|
+
import re
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolProgress, ToolResult
|
|
11
|
+
|
|
12
|
+
_MAX_MATCHES = 100
|
|
13
|
+
_MAX_FILES_SCANNED = 500
|
|
14
|
+
_PROGRESS_INTERVAL = 100 # emit a progress event every N files scanned
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GrepSearchInput(BaseModel):
|
|
18
|
+
pattern: str
|
|
19
|
+
path: str = "."
|
|
20
|
+
glob: str = "**/*"
|
|
21
|
+
context: int = 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GrepSearchTool(Tool):
|
|
25
|
+
@property
|
|
26
|
+
def name(self) -> str:
|
|
27
|
+
return "grep_search"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def description(self) -> str:
|
|
31
|
+
return (
|
|
32
|
+
"Search for a regex pattern across files in a directory. "
|
|
33
|
+
"Returns up to 100 matches across up to 500 files."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def input_schema(self) -> dict:
|
|
38
|
+
return {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"pattern": {"type": "string", "description": "Regex pattern to search for"},
|
|
42
|
+
"path": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "Directory to search in (default: current dir)",
|
|
45
|
+
},
|
|
46
|
+
"glob": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Glob filter for filenames (e.g. *.py)",
|
|
49
|
+
},
|
|
50
|
+
"context": {
|
|
51
|
+
"type": "integer",
|
|
52
|
+
"description": "Lines of context to include before and after each match",
|
|
53
|
+
"default": 0,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
"required": ["pattern"],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def required_permission(self) -> PermissionLevel:
|
|
61
|
+
return PermissionLevel.READ_ONLY
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def input_model(self) -> type[GrepSearchInput]:
|
|
65
|
+
return GrepSearchInput
|
|
66
|
+
|
|
67
|
+
def is_read_only(self, args: dict) -> bool:
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
def is_concurrency_safe(self, args: dict) -> bool:
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
def execute(self, args: dict) -> ToolResult:
|
|
74
|
+
return self._search(args, on_progress=None)
|
|
75
|
+
|
|
76
|
+
def execute_with_progress(
|
|
77
|
+
self,
|
|
78
|
+
args: dict,
|
|
79
|
+
on_progress: Callable[[ToolProgress], None],
|
|
80
|
+
) -> ToolResult:
|
|
81
|
+
return self._search(args, on_progress=on_progress)
|
|
82
|
+
|
|
83
|
+
def _search(
|
|
84
|
+
self,
|
|
85
|
+
args: dict,
|
|
86
|
+
on_progress: Callable[[ToolProgress], None] | None,
|
|
87
|
+
) -> ToolResult:
|
|
88
|
+
pattern_str: str = args["pattern"]
|
|
89
|
+
search_path = pathlib.Path(args.get("path", "."))
|
|
90
|
+
glob_filter: str = args.get("glob", "**/*")
|
|
91
|
+
context_lines: int = int(args.get("context", 0))
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
regex = re.compile(pattern_str)
|
|
95
|
+
except re.error as exc:
|
|
96
|
+
return ToolResult(output=f"Invalid regex: {exc}", is_error=True)
|
|
97
|
+
|
|
98
|
+
# Collect candidate files
|
|
99
|
+
try:
|
|
100
|
+
candidates = [p for p in search_path.glob(glob_filter) if p.is_file()]
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
return ToolResult(output=f"Glob error: {exc}", is_error=True)
|
|
103
|
+
|
|
104
|
+
candidates = candidates[:_MAX_FILES_SCANNED]
|
|
105
|
+
total = len(candidates)
|
|
106
|
+
|
|
107
|
+
results: list[str] = []
|
|
108
|
+
match_count = 0
|
|
109
|
+
|
|
110
|
+
for file_idx, file_path in enumerate(candidates, start=1):
|
|
111
|
+
if match_count >= _MAX_MATCHES:
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
# Emit progress every PROGRESS_INTERVAL files
|
|
115
|
+
if on_progress is not None and file_idx % _PROGRESS_INTERVAL == 0:
|
|
116
|
+
percent = round(file_idx / total * 100.0, 1) if total else 100.0
|
|
117
|
+
on_progress(
|
|
118
|
+
ToolProgress(
|
|
119
|
+
tool_name=self.name,
|
|
120
|
+
message=f"Scanned {file_idx}/{total} files",
|
|
121
|
+
percent=percent,
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
lines = file_path.read_text(errors="replace").splitlines()
|
|
127
|
+
except Exception:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
for i, line in enumerate(lines):
|
|
131
|
+
if match_count >= _MAX_MATCHES:
|
|
132
|
+
break
|
|
133
|
+
if regex.search(line):
|
|
134
|
+
# Gather context
|
|
135
|
+
start = max(0, i - context_lines)
|
|
136
|
+
end = min(len(lines), i + context_lines + 1)
|
|
137
|
+
block = [f"{file_path}:{start + j + 1}: {lines[start + j]}" for j in range(end - start)]
|
|
138
|
+
results.append("\n".join(block))
|
|
139
|
+
match_count += 1
|
|
140
|
+
|
|
141
|
+
if not results:
|
|
142
|
+
return ToolResult(output=f"No matches found for: {pattern_str}")
|
|
143
|
+
|
|
144
|
+
return ToolResult(output="\n---\n".join(results))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""IDEDiagnosticsTool — get diagnostics from the connected IDE."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
from llm_code.ide.bridge import IDEBridge
|
|
7
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IDEDiagnosticsTool(Tool):
|
|
11
|
+
def __init__(self, bridge: IDEBridge) -> None:
|
|
12
|
+
self._bridge = bridge
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def name(self) -> str:
|
|
16
|
+
return "ide_diagnostics"
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def description(self) -> str:
|
|
20
|
+
return "Get diagnostics (errors, warnings) for a file from the connected IDE."
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def input_schema(self) -> dict:
|
|
24
|
+
return {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"path": {"type": "string", "description": "Absolute path to the file"},
|
|
28
|
+
},
|
|
29
|
+
"required": ["path"],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def required_permission(self) -> PermissionLevel:
|
|
34
|
+
return PermissionLevel.READ_ONLY
|
|
35
|
+
|
|
36
|
+
def is_read_only(self, args: dict) -> bool:
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
def is_concurrency_safe(self, args: dict) -> bool:
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
def execute(self, args: dict) -> ToolResult:
|
|
43
|
+
path = args["path"]
|
|
44
|
+
loop = asyncio.get_event_loop()
|
|
45
|
+
diags = loop.run_until_complete(self._bridge.get_diagnostics(path))
|
|
46
|
+
|
|
47
|
+
if not diags:
|
|
48
|
+
return ToolResult(output=f"No diagnostics for {path}.")
|
|
49
|
+
|
|
50
|
+
lines: list[str] = [f"Diagnostics for {path} ({len(diags)} issues):"]
|
|
51
|
+
for d in diags:
|
|
52
|
+
line_num = d.get("line", "?")
|
|
53
|
+
severity = d.get("severity", "info")
|
|
54
|
+
message = d.get("message", "")
|
|
55
|
+
source = d.get("source", "")
|
|
56
|
+
src_str = f" [{source}]" if source else ""
|
|
57
|
+
lines.append(f" L{line_num} {severity}: {message}{src_str}")
|
|
58
|
+
|
|
59
|
+
return ToolResult(output="\n".join(lines))
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""IDEOpenTool — ask the connected IDE to open a file."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
from llm_code.ide.bridge import IDEBridge
|
|
7
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IDEOpenTool(Tool):
|
|
11
|
+
def __init__(self, bridge: IDEBridge) -> None:
|
|
12
|
+
self._bridge = bridge
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def name(self) -> str:
|
|
16
|
+
return "ide_open"
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def description(self) -> str:
|
|
20
|
+
return "Open a file in the connected IDE at an optional line number."
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def input_schema(self) -> dict:
|
|
24
|
+
return {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"path": {"type": "string", "description": "Absolute path to the file"},
|
|
28
|
+
"line": {
|
|
29
|
+
"type": "integer",
|
|
30
|
+
"description": "Line number to jump to (optional)",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
"required": ["path"],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def required_permission(self) -> PermissionLevel:
|
|
38
|
+
return PermissionLevel.READ_ONLY
|
|
39
|
+
|
|
40
|
+
def is_read_only(self, args: dict) -> bool:
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
def is_concurrency_safe(self, args: dict) -> bool:
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
def execute(self, args: dict) -> ToolResult:
|
|
47
|
+
if not self._bridge.is_connected:
|
|
48
|
+
return ToolResult(output="No IDE connected. Use /ide connect first.", is_error=True)
|
|
49
|
+
|
|
50
|
+
path = args["path"]
|
|
51
|
+
line = args.get("line")
|
|
52
|
+
loop = asyncio.get_event_loop()
|
|
53
|
+
ok = loop.run_until_complete(self._bridge.open_file(path, line=line))
|
|
54
|
+
|
|
55
|
+
if ok:
|
|
56
|
+
line_str = f" at line {line}" if line else ""
|
|
57
|
+
return ToolResult(output=f"Opened {path}{line_str} in IDE.")
|
|
58
|
+
return ToolResult(output=f"Failed to open {path} in IDE.", is_error=True)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""IDESelectionTool — get the current editor selection from the connected IDE."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
from llm_code.ide.bridge import IDEBridge
|
|
7
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IDESelectionTool(Tool):
|
|
11
|
+
def __init__(self, bridge: IDEBridge) -> None:
|
|
12
|
+
self._bridge = bridge
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def name(self) -> str:
|
|
16
|
+
return "ide_selection"
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def description(self) -> str:
|
|
20
|
+
return "Get the currently selected text in the connected IDE's editor."
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def input_schema(self) -> dict:
|
|
24
|
+
return {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def required_permission(self) -> PermissionLevel:
|
|
31
|
+
return PermissionLevel.READ_ONLY
|
|
32
|
+
|
|
33
|
+
def is_read_only(self, args: dict) -> bool:
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
def is_concurrency_safe(self, args: dict) -> bool:
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
def execute(self, args: dict) -> ToolResult:
|
|
40
|
+
loop = asyncio.get_event_loop()
|
|
41
|
+
sel = loop.run_until_complete(self._bridge.get_selection())
|
|
42
|
+
|
|
43
|
+
if sel is None:
|
|
44
|
+
return ToolResult(output="No selection — no IDE connected or nothing selected.")
|
|
45
|
+
|
|
46
|
+
path = sel.get("path", "unknown")
|
|
47
|
+
start = sel.get("start_line", "?")
|
|
48
|
+
end = sel.get("end_line", "?")
|
|
49
|
+
text = sel.get("text", "")
|
|
50
|
+
|
|
51
|
+
header = f"Selection in {path} (lines {start}-{end}):"
|
|
52
|
+
return ToolResult(output=f"{header}\n{text}")
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Memory tools: store, recall, and list cross-session memory entries."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from llm_code.runtime.memory import MemoryStore
|
|
7
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MemoryStoreInput(BaseModel):
|
|
11
|
+
key: str
|
|
12
|
+
value: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MemoryRecallInput(BaseModel):
|
|
16
|
+
key: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MemoryStoreTool(Tool):
|
|
20
|
+
"""Store a value in persistent memory under a given key."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, memory: MemoryStore) -> None:
|
|
23
|
+
self._memory = memory
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def name(self) -> str:
|
|
27
|
+
return "memory_store"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def description(self) -> str:
|
|
31
|
+
return "Store a value in persistent cross-session memory under a given key."
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def input_schema(self) -> dict:
|
|
35
|
+
return {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"key": {"type": "string", "description": "The memory key"},
|
|
39
|
+
"value": {"type": "string", "description": "The value to store"},
|
|
40
|
+
},
|
|
41
|
+
"required": ["key", "value"],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def required_permission(self) -> PermissionLevel:
|
|
46
|
+
return PermissionLevel.WORKSPACE_WRITE
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def input_model(self) -> type[MemoryStoreInput]:
|
|
50
|
+
return MemoryStoreInput
|
|
51
|
+
|
|
52
|
+
def execute(self, args: dict) -> ToolResult:
|
|
53
|
+
self._memory.store(args["key"], args["value"])
|
|
54
|
+
return ToolResult(output=f"Stored: {args['key']}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class MemoryRecallTool(Tool):
|
|
58
|
+
"""Recall a value from persistent memory by key."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, memory: MemoryStore) -> None:
|
|
61
|
+
self._memory = memory
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def name(self) -> str:
|
|
65
|
+
return "memory_recall"
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def description(self) -> str:
|
|
69
|
+
return "Recall a value from persistent cross-session memory by key."
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def input_schema(self) -> dict:
|
|
73
|
+
return {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"properties": {
|
|
76
|
+
"key": {"type": "string", "description": "The memory key to recall"},
|
|
77
|
+
},
|
|
78
|
+
"required": ["key"],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def required_permission(self) -> PermissionLevel:
|
|
83
|
+
return PermissionLevel.READ_ONLY
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def input_model(self) -> type[MemoryRecallInput]:
|
|
87
|
+
return MemoryRecallInput
|
|
88
|
+
|
|
89
|
+
def is_read_only(self, args: dict) -> bool:
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
def is_concurrency_safe(self, args: dict) -> bool:
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
def execute(self, args: dict) -> ToolResult:
|
|
96
|
+
value = self._memory.recall(args["key"])
|
|
97
|
+
if value is None:
|
|
98
|
+
return ToolResult(output=f"No memory found for key: {args['key']}", is_error=True)
|
|
99
|
+
return ToolResult(output=value)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class MemoryListTool(Tool):
|
|
103
|
+
"""List all keys and values stored in persistent memory."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, memory: MemoryStore) -> None:
|
|
106
|
+
self._memory = memory
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def name(self) -> str:
|
|
110
|
+
return "memory_list"
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def description(self) -> str:
|
|
114
|
+
return "List all keys and values stored in persistent cross-session memory."
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def input_schema(self) -> dict:
|
|
118
|
+
return {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"properties": {},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def required_permission(self) -> PermissionLevel:
|
|
125
|
+
return PermissionLevel.READ_ONLY
|
|
126
|
+
|
|
127
|
+
def is_read_only(self, args: dict) -> bool:
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
def execute(self, args: dict) -> ToolResult:
|
|
131
|
+
entries = self._memory.get_all()
|
|
132
|
+
if not entries:
|
|
133
|
+
return ToolResult(output="No memories stored.")
|
|
134
|
+
lines = [
|
|
135
|
+
f"- {k}: {v.value[:50]}..." if len(v.value) > 50 else f"- {k}: {v.value}"
|
|
136
|
+
for k, v in entries.items()
|
|
137
|
+
]
|
|
138
|
+
return ToolResult(output="\n".join(lines))
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""MultiEditTool — atomic multi-file search-and-replace."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pathlib
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from llm_code.runtime.file_protection import check_write
|
|
10
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
11
|
+
from llm_code.tools.edit_file import _apply_edit
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from llm_code.runtime.overlay import OverlayFS
|
|
15
|
+
|
|
16
|
+
_MAX_EDITS = 20
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SingleEdit(BaseModel):
|
|
20
|
+
path: str
|
|
21
|
+
old: str
|
|
22
|
+
new: str
|
|
23
|
+
replace_all: bool = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MultiEditInput(BaseModel):
|
|
27
|
+
edits: list[SingleEdit]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MultiEditTool(Tool):
|
|
31
|
+
@property
|
|
32
|
+
def name(self) -> str:
|
|
33
|
+
return "multi_edit"
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def description(self) -> str:
|
|
37
|
+
return "Atomic multi-file search-and-replace. All edits succeed or none are applied."
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def input_schema(self) -> dict:
|
|
41
|
+
return {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"edits": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {
|
|
49
|
+
"path": {"type": "string", "description": "Absolute path to file"},
|
|
50
|
+
"old": {"type": "string", "description": "Text to search for"},
|
|
51
|
+
"new": {"type": "string", "description": "Replacement text"},
|
|
52
|
+
"replace_all": {"type": "boolean", "default": False},
|
|
53
|
+
},
|
|
54
|
+
"required": ["path", "old", "new"],
|
|
55
|
+
},
|
|
56
|
+
"minItems": 1,
|
|
57
|
+
"maxItems": _MAX_EDITS,
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"required": ["edits"],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def required_permission(self) -> PermissionLevel:
|
|
65
|
+
return PermissionLevel.WORKSPACE_WRITE
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def input_model(self) -> type[MultiEditInput]:
|
|
69
|
+
return MultiEditInput
|
|
70
|
+
|
|
71
|
+
def execute(self, args: dict, overlay: "OverlayFS | None" = None) -> ToolResult:
|
|
72
|
+
edits_raw = args.get("edits", [])
|
|
73
|
+
|
|
74
|
+
if len(edits_raw) > _MAX_EDITS:
|
|
75
|
+
return ToolResult(
|
|
76
|
+
output=f"Too many edits ({len(edits_raw)}). Maximum is {_MAX_EDITS}.",
|
|
77
|
+
is_error=True,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
edits = [SingleEdit(**e) if isinstance(e, dict) else e for e in edits_raw]
|
|
81
|
+
|
|
82
|
+
# Phase 1: Pre-validate (existence + write permission)
|
|
83
|
+
errors: list[str] = []
|
|
84
|
+
for i, edit in enumerate(edits):
|
|
85
|
+
path = pathlib.Path(edit.path)
|
|
86
|
+
if overlay is None:
|
|
87
|
+
if not path.exists():
|
|
88
|
+
errors.append(f"Edit {i + 1}: File not found: {path}")
|
|
89
|
+
continue
|
|
90
|
+
else:
|
|
91
|
+
try:
|
|
92
|
+
overlay.read(path)
|
|
93
|
+
except FileNotFoundError:
|
|
94
|
+
errors.append(f"Edit {i + 1}: File not found: {path}")
|
|
95
|
+
continue
|
|
96
|
+
protection = check_write(str(path))
|
|
97
|
+
if not protection.allowed:
|
|
98
|
+
errors.append(f"Edit {i + 1}: {protection.reason}")
|
|
99
|
+
if errors:
|
|
100
|
+
return ToolResult(output="Validation failed:\n" + "\n".join(errors), is_error=True)
|
|
101
|
+
|
|
102
|
+
# Phase 2: Snapshot original contents
|
|
103
|
+
snapshots: dict[str, str] = {}
|
|
104
|
+
for edit in edits:
|
|
105
|
+
p = str(edit.path)
|
|
106
|
+
if p not in snapshots:
|
|
107
|
+
path = pathlib.Path(p)
|
|
108
|
+
if overlay is not None:
|
|
109
|
+
snapshots[p] = overlay.read(path)
|
|
110
|
+
else:
|
|
111
|
+
snapshots[p] = path.read_text(encoding="utf-8")
|
|
112
|
+
|
|
113
|
+
# Phase 3: Apply all edits in memory
|
|
114
|
+
applied: list[str] = []
|
|
115
|
+
current_contents: dict[str, str] = dict(snapshots)
|
|
116
|
+
for i, edit in enumerate(edits):
|
|
117
|
+
p = str(edit.path)
|
|
118
|
+
result = _apply_edit(current_contents[p], edit.old, edit.new, edit.replace_all)
|
|
119
|
+
if not result.success:
|
|
120
|
+
# Rollback: restore snapshots to real FS (overlay needs no rollback
|
|
121
|
+
# — caller discards the overlay on failure)
|
|
122
|
+
if overlay is None:
|
|
123
|
+
for sp, sc in snapshots.items():
|
|
124
|
+
pathlib.Path(sp).write_text(sc, encoding="utf-8")
|
|
125
|
+
return ToolResult(
|
|
126
|
+
output=f"Edit {i + 1} failed ({edit.path}): {result.error}. All edits rolled back.",
|
|
127
|
+
is_error=True,
|
|
128
|
+
)
|
|
129
|
+
current_contents[p] = result.new_content
|
|
130
|
+
applied.append(f"Edit {i + 1}: {edit.path} ({result.replaced} replacement(s))")
|
|
131
|
+
|
|
132
|
+
# Phase 4: Write all files
|
|
133
|
+
for p, content in current_contents.items():
|
|
134
|
+
path = pathlib.Path(p)
|
|
135
|
+
if overlay is not None:
|
|
136
|
+
overlay.write(path, content)
|
|
137
|
+
else:
|
|
138
|
+
path.write_text(content, encoding="utf-8")
|
|
139
|
+
|
|
140
|
+
return ToolResult(
|
|
141
|
+
output=f"Applied {len(edits)} edits:\n" + "\n".join(applied),
|
|
142
|
+
metadata={"edits_applied": len(edits)},
|
|
143
|
+
)
|