bamboo-coding 0.1.0__tar.gz → 0.1.2__tar.gz
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.
- bamboo_coding-0.1.2/PKG-INFO +29 -0
- bamboo_coding-0.1.2/README.md +18 -0
- bamboo_coding-0.1.2/pyproject.toml +23 -0
- {bamboo_coding-0.1.0/bamboo_coding/client → bamboo_coding-0.1.2/src/bamboo_coding}/capabilities/repo.py +17 -2
- {bamboo_coding-0.1.0/bamboo_coding/client → bamboo_coding-0.1.2/src/bamboo_coding}/config.py +79 -12
- {bamboo_coding-0.1.0/bamboo_coding/client → bamboo_coding-0.1.2/src/bamboo_coding}/controller_client.py +31 -3
- {bamboo_coding-0.1.0 → bamboo_coding-0.1.2/src/bamboo_coding}/git_utils.py +145 -0
- bamboo_coding-0.1.2/src/bamboo_coding/main.py +774 -0
- {bamboo_coding-0.1.0/shared → bamboo_coding-0.1.2/src/bamboo_coding}/protocol.py +31 -0
- bamboo_coding-0.1.2/src/bamboo_coding/scrollback_buffer.py +53 -0
- {bamboo_coding-0.1.0/agent/bamboo_git_agent → bamboo_coding-0.1.2/src/bamboo_coding}/status.py +1 -1
- {bamboo_coding-0.1.0/bamboo_coding/client → bamboo_coding-0.1.2/src/bamboo_coding}/terminal_runtime.py +90 -4
- bamboo_coding-0.1.2/src/bamboo_coding.egg-info/PKG-INFO +29 -0
- bamboo_coding-0.1.2/src/bamboo_coding.egg-info/SOURCES.txt +20 -0
- bamboo_coding-0.1.2/src/bamboo_coding.egg-info/entry_points.txt +2 -0
- bamboo_coding-0.1.2/src/bamboo_coding.egg-info/requires.txt +4 -0
- bamboo_coding-0.1.2/src/bamboo_coding.egg-info/top_level.txt +1 -0
- bamboo_coding-0.1.0/PKG-INFO +0 -284
- bamboo_coding-0.1.0/README.md +0 -270
- bamboo_coding-0.1.0/agent/__init__.py +0 -0
- bamboo_coding-0.1.0/agent/bamboo_git_agent/__init__.py +0 -0
- bamboo_coding-0.1.0/agent/bamboo_git_agent/capabilities/__init__.py +0 -0
- bamboo_coding-0.1.0/agent/bamboo_git_agent/capabilities/repo.py +0 -189
- bamboo_coding-0.1.0/agent/bamboo_git_agent/config.py +0 -76
- bamboo_coding-0.1.0/agent/bamboo_git_agent/controller_client.py +0 -73
- bamboo_coding-0.1.0/agent/bamboo_git_agent/main.py +0 -164
- bamboo_coding-0.1.0/agent/bamboo_git_agent/protocol.py +0 -21
- bamboo_coding-0.1.0/api/__init__.py +0 -2
- bamboo_coding-0.1.0/api/commit.py +0 -82
- bamboo_coding-0.1.0/api/repository.py +0 -592
- bamboo_coding-0.1.0/bamboo_coding/__init__.py +0 -3
- bamboo_coding-0.1.0/bamboo_coding/client/journal.py +0 -1
- bamboo_coding-0.1.0/bamboo_coding/client/main.py +0 -425
- bamboo_coding-0.1.0/bamboo_coding/client/status.py +0 -59
- bamboo_coding-0.1.0/bamboo_coding/server/__init__.py +0 -3
- bamboo_coding-0.1.0/bamboo_coding/server/core/__init__.py +0 -1
- bamboo_coding-0.1.0/bamboo_coding/server/core/config.py +0 -1
- bamboo_coding-0.1.0/bamboo_coding/server/main.py +0 -39
- bamboo_coding-0.1.0/bamboo_coding/shared/__init__.py +0 -39
- bamboo_coding-0.1.0/bamboo_coding/shared/protocol.py +0 -1
- bamboo_coding-0.1.0/bamboo_coding.egg-info/PKG-INFO +0 -284
- bamboo_coding-0.1.0/bamboo_coding.egg-info/SOURCES.txt +0 -70
- bamboo_coding-0.1.0/bamboo_coding.egg-info/entry_points.txt +0 -3
- bamboo_coding-0.1.0/bamboo_coding.egg-info/requires.txt +0 -7
- bamboo_coding-0.1.0/bamboo_coding.egg-info/top_level.txt +0 -6
- bamboo_coding-0.1.0/controller/__init__.py +0 -0
- bamboo_coding-0.1.0/controller/app/__init__.py +0 -0
- bamboo_coding-0.1.0/controller/app/api/__init__.py +0 -0
- bamboo_coding-0.1.0/controller/app/api/agents/__init__.py +0 -3
- bamboo_coding-0.1.0/controller/app/api/agents/ws.py +0 -133
- bamboo_coding-0.1.0/controller/app/api/public/__init__.py +0 -11
- bamboo_coding-0.1.0/controller/app/api/public/repos.py +0 -207
- bamboo_coding-0.1.0/controller/app/api/public/terminals.py +0 -64
- bamboo_coding-0.1.0/controller/app/core/__init__.py +0 -0
- bamboo_coding-0.1.0/controller/app/core/config.py +0 -20
- bamboo_coding-0.1.0/controller/app/core/errors.py +0 -14
- bamboo_coding-0.1.0/controller/app/db/__init__.py +0 -0
- bamboo_coding-0.1.0/controller/app/db/models.py +0 -42
- bamboo_coding-0.1.0/controller/app/db/session.py +0 -74
- bamboo_coding-0.1.0/controller/app/main.py +0 -71
- bamboo_coding-0.1.0/controller/app/schemas/__init__.py +0 -23
- bamboo_coding-0.1.0/controller/app/schemas/agent_messages.py +0 -21
- bamboo_coding-0.1.0/controller/app/schemas/public.py +0 -3
- bamboo_coding-0.1.0/controller/app/services/__init__.py +0 -0
- bamboo_coding-0.1.0/controller/app/services/agents.py +0 -108
- bamboo_coding-0.1.0/controller/app/services/registrations.py +0 -17
- bamboo_coding-0.1.0/controller/app/services/repositories.py +0 -85
- bamboo_coding-0.1.0/controller/app/services/router.py +0 -140
- bamboo_coding-0.1.0/controller/app/services/tasks.py +0 -104
- bamboo_coding-0.1.0/controller/app/services/terminals.py +0 -148
- bamboo_coding-0.1.0/pyproject.toml +0 -29
- bamboo_coding-0.1.0/shared/__init__.py +0 -39
- bamboo_coding-0.1.0/tests/test_api.py +0 -140
- bamboo_coding-0.1.0/tests/test_container_contract.py +0 -7
- bamboo_coding-0.1.0/tests/test_frontend_contract.py +0 -3275
- bamboo_coding-0.1.0/tests/test_git_utils.py +0 -286
- bamboo_coding-0.1.0/tests/test_makefile.py +0 -62
- {bamboo_coding-0.1.0 → bamboo_coding-0.1.2}/setup.cfg +0 -0
- {bamboo_coding-0.1.0/bamboo_coding/client → bamboo_coding-0.1.2/src/bamboo_coding}/__init__.py +0 -0
- {bamboo_coding-0.1.0/bamboo_coding/client → bamboo_coding-0.1.2/src/bamboo_coding}/capabilities/__init__.py +0 -0
- {bamboo_coding-0.1.0/agent/bamboo_git_agent → bamboo_coding-0.1.2/src/bamboo_coding}/journal.py +0 -0
- {bamboo_coding-0.1.0 → bamboo_coding-0.1.2/src}/bamboo_coding.egg-info/dependency_links.txt +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bamboo-coding
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Bamboo Coding outbound client
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: gitpython==3.1.40
|
|
8
|
+
Requires-Dist: websockets==13.1
|
|
9
|
+
Requires-Dist: pydantic>=2.0
|
|
10
|
+
Requires-Dist: bamboo-ssh
|
|
11
|
+
|
|
12
|
+
# bamboo-coding
|
|
13
|
+
|
|
14
|
+
Outbound client for the Bamboo Coding controller.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install bamboo-coding
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bamboo-coding setup # configure default profile
|
|
26
|
+
bamboo-coding run # connect to the controller
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The client reads its configuration from `~/.bamboo-coding/bamboo-coding.toml`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# bamboo-coding
|
|
2
|
+
|
|
3
|
+
Outbound client for the Bamboo Coding controller.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install bamboo-coding
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bamboo-coding setup # configure default profile
|
|
15
|
+
bamboo-coding run # connect to the controller
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The client reads its configuration from `~/.bamboo-coding/bamboo-coding.toml`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bamboo-coding"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Bamboo Coding outbound client"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"gitpython==3.1.40",
|
|
13
|
+
"websockets==13.1",
|
|
14
|
+
"pydantic>=2.0",
|
|
15
|
+
"bamboo-ssh",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
bamboo-coding = "bamboo_coding.main:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
23
|
+
include = ["bamboo_coding*"]
|
|
@@ -3,8 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Callable
|
|
5
5
|
|
|
6
|
-
import git_utils
|
|
7
|
-
from
|
|
6
|
+
from .. import git_utils
|
|
7
|
+
from ..protocol import TaskMessage
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def _allowed_roots(repositories: dict[str, str] | list[str] | tuple[str, ...]) -> list[str]:
|
|
@@ -229,6 +229,19 @@ def _pull_remote(task: TaskMessage, repositories) -> dict:
|
|
|
229
229
|
)
|
|
230
230
|
|
|
231
231
|
|
|
232
|
+
def _search(task: TaskMessage, repositories) -> dict:
|
|
233
|
+
repo_path = _resolve_repo_root(task, repositories)
|
|
234
|
+
query = str(task.params.get("query", ""))
|
|
235
|
+
return git_utils.search_repository(
|
|
236
|
+
repo_path,
|
|
237
|
+
query,
|
|
238
|
+
case_sensitive=bool(task.params.get("case_sensitive", False)),
|
|
239
|
+
whole_word=bool(task.params.get("whole_word", False)),
|
|
240
|
+
regex=bool(task.params.get("regex", False)),
|
|
241
|
+
max_matches=int(task.params.get("max_matches", 1000)),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
232
245
|
_HANDLERS: dict[tuple[str, str], Callable[[TaskMessage, dict[str, str] | list[str] | tuple[str, ...]], dict]] = {
|
|
233
246
|
("fs.tree", "get"): _fs_tree,
|
|
234
247
|
("fs.browse", "get"): _fs_browse,
|
|
@@ -276,4 +289,6 @@ _HANDLERS: dict[tuple[str, str], Callable[[TaskMessage, dict[str, str] | list[st
|
|
|
276
289
|
("git.branches.delete", "delete"): _delete_branch,
|
|
277
290
|
("git.branches.merge", "post"): _merge_branch,
|
|
278
291
|
("git.commit.create", "post"): _create_commit,
|
|
292
|
+
("git.search", "post"): _search,
|
|
293
|
+
("repo.search", "post"): _search,
|
|
279
294
|
}
|
{bamboo_coding-0.1.0/bamboo_coding/client → bamboo_coding-0.1.2/src/bamboo_coding}/config.py
RENAMED
|
@@ -13,6 +13,8 @@ DEFAULT_CONFIG_DIRNAME = ".bamboo-coding"
|
|
|
13
13
|
DEFAULT_CONFIG_FILENAME = "bamboo-coding.toml"
|
|
14
14
|
DEFAULT_CONTROLLER_URL = "ws://127.0.0.1:8100/ws/agents"
|
|
15
15
|
DEFAULT_CLIENT_ID = "bamboo-coding-client"
|
|
16
|
+
DEFAULT_PROFILE = "default"
|
|
17
|
+
SERVERS_DIRNAME = "servers"
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
@dataclass(frozen=True)
|
|
@@ -32,6 +34,10 @@ class AgentConfig:
|
|
|
32
34
|
token_path: Path
|
|
33
35
|
journal_path: Path
|
|
34
36
|
config_path: Path = field(default_factory=Path)
|
|
37
|
+
profile: str = DEFAULT_PROFILE
|
|
38
|
+
profile_exists: bool = True
|
|
39
|
+
legacy_client_section: bool = False
|
|
40
|
+
available_profiles: tuple[str, ...] = field(default_factory=tuple)
|
|
35
41
|
max_concurrent_tasks: int = 1
|
|
36
42
|
allowed_roots: tuple[str, ...] = field(default_factory=tuple)
|
|
37
43
|
repositories: tuple[RepositoryConfig, ...] = field(default_factory=tuple)
|
|
@@ -107,7 +113,39 @@ def _load_legacy_token(default_token_path: Path) -> str:
|
|
|
107
113
|
return token
|
|
108
114
|
|
|
109
115
|
|
|
110
|
-
def
|
|
116
|
+
def _resolve_profile_section(raw_config: dict, profile: str) -> tuple[dict | None, bool]:
|
|
117
|
+
"""Return (profile_dict, is_legacy_client_section).
|
|
118
|
+
|
|
119
|
+
profile_dict is None if no matching profile was found.
|
|
120
|
+
"""
|
|
121
|
+
servers = raw_config.get("servers")
|
|
122
|
+
if isinstance(servers, dict):
|
|
123
|
+
candidate = servers.get(profile)
|
|
124
|
+
if isinstance(candidate, dict):
|
|
125
|
+
return candidate, False
|
|
126
|
+
if profile == DEFAULT_PROFILE:
|
|
127
|
+
legacy = raw_config.get("client")
|
|
128
|
+
if isinstance(legacy, dict):
|
|
129
|
+
return legacy, True
|
|
130
|
+
return None, False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _list_available_profiles(raw_config: dict) -> tuple[str, ...]:
|
|
134
|
+
names: list[str] = []
|
|
135
|
+
servers = raw_config.get("servers")
|
|
136
|
+
if isinstance(servers, dict):
|
|
137
|
+
names.extend(name for name in servers if isinstance(servers.get(name), dict))
|
|
138
|
+
if DEFAULT_PROFILE not in names and isinstance(raw_config.get("client"), dict):
|
|
139
|
+
names.insert(0, DEFAULT_PROFILE)
|
|
140
|
+
return tuple(names)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _profile_storage_dir(storage_home: Path, profile: str) -> Path:
|
|
144
|
+
return storage_home / SERVERS_DIRNAME / profile
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def load_config(profile: str | None = None) -> AgentConfig:
|
|
148
|
+
active_profile = profile or DEFAULT_PROFILE
|
|
111
149
|
config_home = Path(_first_env("BAMBOO_CODING_HOME") or (Path.home() / DEFAULT_CONFIG_DIRNAME)).expanduser().resolve()
|
|
112
150
|
config_path = _resolve_path(
|
|
113
151
|
_first_env("BAMBOO_CODING_CONFIG_PATH"),
|
|
@@ -115,45 +153,70 @@ def load_config() -> AgentConfig:
|
|
|
115
153
|
base_dir=config_home,
|
|
116
154
|
)
|
|
117
155
|
raw_config = _load_toml(config_path)
|
|
118
|
-
|
|
119
|
-
|
|
156
|
+
profile_section, is_legacy_client = _resolve_profile_section(raw_config, active_profile)
|
|
157
|
+
available_profiles = _list_available_profiles(raw_config)
|
|
158
|
+
profile_exists = profile_section is not None
|
|
159
|
+
section = profile_section if isinstance(profile_section, dict) else {}
|
|
160
|
+
|
|
161
|
+
explicit_new_layout = profile_exists and not is_legacy_client
|
|
162
|
+
storage_config = (
|
|
163
|
+
raw_config.get("storage")
|
|
164
|
+
if not explicit_new_layout and isinstance(raw_config.get("storage"), dict)
|
|
165
|
+
else {}
|
|
166
|
+
)
|
|
120
167
|
|
|
121
168
|
storage_home_value = _first_env("BAMBOO_CODING_HOME", "AGENT_HOME")
|
|
122
169
|
storage_home = Path(storage_home_value).expanduser().resolve() if storage_home_value else config_home
|
|
123
170
|
|
|
171
|
+
if explicit_new_layout:
|
|
172
|
+
profile_dir = _profile_storage_dir(storage_home, active_profile)
|
|
173
|
+
default_token_path = profile_dir / "token.json"
|
|
174
|
+
default_journal_path = profile_dir / "journal.db"
|
|
175
|
+
elif active_profile == DEFAULT_PROFILE:
|
|
176
|
+
default_token_path = storage_home / "token.json"
|
|
177
|
+
default_journal_path = storage_home / "journal.db"
|
|
178
|
+
else:
|
|
179
|
+
profile_dir = _profile_storage_dir(storage_home, active_profile)
|
|
180
|
+
default_token_path = profile_dir / "token.json"
|
|
181
|
+
default_journal_path = profile_dir / "journal.db"
|
|
182
|
+
|
|
124
183
|
controller_url = _first_env("BAMBOO_CODING_CONTROLLER_URL", "CONTROLLER_URL") or str(
|
|
125
|
-
|
|
184
|
+
section.get("controller_url") or DEFAULT_CONTROLLER_URL
|
|
126
185
|
)
|
|
127
186
|
client_id = _first_env("BAMBOO_CODING_CLIENT_ID", "AGENT_CLIENT_ID") or str(
|
|
128
|
-
|
|
187
|
+
section.get("client_id") or platform.node() or DEFAULT_CLIENT_ID
|
|
129
188
|
)
|
|
130
189
|
hostname = _first_env("BAMBOO_CODING_HOSTNAME", "AGENT_HOSTNAME") or str(
|
|
131
|
-
|
|
190
|
+
section.get("hostname") or platform.node() or client_id
|
|
132
191
|
)
|
|
133
|
-
version = _first_env("BAMBOO_CODING_VERSION", "AGENT_VERSION") or str(
|
|
192
|
+
version = _first_env("BAMBOO_CODING_VERSION", "AGENT_VERSION") or str(section.get("version") or _default_version())
|
|
134
193
|
allowed_roots = _coerce_allowed_roots(
|
|
135
194
|
_first_env("BAMBOO_CODING_ALLOWED_ROOTS", "AGENT_ALLOWED_ROOTS", "AGENT_ROOT", "AGENT_REPO_PATH")
|
|
136
|
-
or
|
|
195
|
+
or section.get("allowed_roots")
|
|
137
196
|
)
|
|
138
197
|
max_concurrent_tasks = int(
|
|
139
198
|
_first_env("BAMBOO_CODING_MAX_CONCURRENT_TASKS", "AGENT_MAX_CONCURRENT_TASKS")
|
|
140
|
-
or
|
|
199
|
+
or section.get("max_concurrent_tasks")
|
|
141
200
|
or 1
|
|
142
201
|
)
|
|
143
202
|
|
|
144
203
|
token_path = _resolve_path(
|
|
145
204
|
_first_env("BAMBOO_CODING_TOKEN_PATH", "AGENT_TOKEN_PATH") or storage_config.get("token_path"),
|
|
146
|
-
default=
|
|
205
|
+
default=default_token_path,
|
|
147
206
|
base_dir=config_path.parent,
|
|
148
207
|
)
|
|
149
208
|
journal_path = _resolve_path(
|
|
150
209
|
_first_env("BAMBOO_CODING_JOURNAL_PATH", "AGENT_JOURNAL_PATH") or storage_config.get("journal_path"),
|
|
151
|
-
default=
|
|
210
|
+
default=default_journal_path,
|
|
152
211
|
base_dir=config_path.parent,
|
|
153
212
|
)
|
|
154
213
|
|
|
155
214
|
token = _first_env("BAMBOO_CODING_TOKEN", "AGENT_TOKEN") or _load_stored_token(token_path)
|
|
156
|
-
if
|
|
215
|
+
if (
|
|
216
|
+
not token
|
|
217
|
+
and active_profile == DEFAULT_PROFILE
|
|
218
|
+
and not _first_env("BAMBOO_CODING_TOKEN_PATH", "AGENT_TOKEN_PATH", "AGENT_HOME")
|
|
219
|
+
):
|
|
157
220
|
token = _load_legacy_token(token_path)
|
|
158
221
|
|
|
159
222
|
repositories = tuple(
|
|
@@ -173,6 +236,10 @@ def load_config() -> AgentConfig:
|
|
|
173
236
|
token_path=token_path,
|
|
174
237
|
journal_path=journal_path,
|
|
175
238
|
config_path=config_path,
|
|
239
|
+
profile=active_profile,
|
|
240
|
+
profile_exists=profile_exists,
|
|
241
|
+
legacy_client_section=is_legacy_client,
|
|
242
|
+
available_profiles=available_profiles,
|
|
176
243
|
max_concurrent_tasks=max_concurrent_tasks,
|
|
177
244
|
allowed_roots=allowed_roots,
|
|
178
245
|
repositories=repositories,
|
|
@@ -2,12 +2,16 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
|
+
import logging
|
|
5
6
|
from typing import Awaitable, Callable
|
|
6
7
|
|
|
7
8
|
from websockets import connect
|
|
8
9
|
from websockets.exceptions import ConnectionClosed
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
from .protocol import (
|
|
11
15
|
AgentRegisterMessage,
|
|
12
16
|
ClientStatusMessage,
|
|
13
17
|
TaskEventMessage,
|
|
@@ -17,15 +21,27 @@ from bamboo_coding.shared.protocol import (
|
|
|
17
21
|
TerminalCloseMessage,
|
|
18
22
|
TerminalInputMessage,
|
|
19
23
|
TerminalHeartbeatMessage,
|
|
24
|
+
TerminalKillMessage,
|
|
25
|
+
TerminalListRequest,
|
|
26
|
+
TerminalListResponse,
|
|
20
27
|
TerminalOpenMessage,
|
|
21
28
|
TerminalOutputMessage,
|
|
22
29
|
TerminalResizeMessage,
|
|
30
|
+
TerminalSignalMessage,
|
|
23
31
|
TerminalStatusMessage,
|
|
24
32
|
TerminalClosedMessage,
|
|
25
33
|
)
|
|
26
34
|
|
|
27
35
|
|
|
28
|
-
TerminalControlMessage =
|
|
36
|
+
TerminalControlMessage = (
|
|
37
|
+
TerminalOpenMessage
|
|
38
|
+
| TerminalInputMessage
|
|
39
|
+
| TerminalResizeMessage
|
|
40
|
+
| TerminalCloseMessage
|
|
41
|
+
| TerminalSignalMessage
|
|
42
|
+
| TerminalKillMessage
|
|
43
|
+
| TerminalListRequest
|
|
44
|
+
)
|
|
29
45
|
|
|
30
46
|
|
|
31
47
|
class ControllerConnection:
|
|
@@ -70,6 +86,9 @@ class ControllerConnection:
|
|
|
70
86
|
async def send_terminal_closed(self, message: TerminalClosedMessage) -> None:
|
|
71
87
|
await self._send(message.model_dump())
|
|
72
88
|
|
|
89
|
+
async def send_terminal_list_response(self, message: TerminalListResponse) -> None:
|
|
90
|
+
await self._send(message.model_dump())
|
|
91
|
+
|
|
73
92
|
async def listen(
|
|
74
93
|
self,
|
|
75
94
|
on_task: Callable[[TaskMessage], Awaitable[None]],
|
|
@@ -91,6 +110,12 @@ class ControllerConnection:
|
|
|
91
110
|
await on_terminal_message(TerminalResizeMessage(**data))
|
|
92
111
|
elif on_terminal_message is not None and message_type == 'terminal_close':
|
|
93
112
|
await on_terminal_message(TerminalCloseMessage(**data))
|
|
113
|
+
elif on_terminal_message is not None and message_type == 'terminal_signal':
|
|
114
|
+
await on_terminal_message(TerminalSignalMessage(**data))
|
|
115
|
+
elif on_terminal_message is not None and message_type == 'terminal_kill':
|
|
116
|
+
await on_terminal_message(TerminalKillMessage(**data))
|
|
117
|
+
elif on_terminal_message is not None and message_type == 'terminal_list':
|
|
118
|
+
await on_terminal_message(TerminalListRequest(**data))
|
|
94
119
|
except ConnectionClosed:
|
|
95
120
|
return
|
|
96
121
|
|
|
@@ -103,7 +128,10 @@ class ControllerConnection:
|
|
|
103
128
|
if self.websocket is None:
|
|
104
129
|
raise RuntimeError('connection not established')
|
|
105
130
|
async with self._send_lock:
|
|
106
|
-
|
|
131
|
+
try:
|
|
132
|
+
await self.websocket.send(json.dumps(payload))
|
|
133
|
+
except ConnectionClosed:
|
|
134
|
+
LOGGER.debug('skipping outbound message; controller connection is closed')
|
|
107
135
|
|
|
108
136
|
async def _receive(self) -> dict:
|
|
109
137
|
if self.websocket is None:
|
|
@@ -14,7 +14,10 @@ Performance:
|
|
|
14
14
|
- Diff context is limited to prevent memory issues with large files
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
import json
|
|
17
18
|
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
18
21
|
import time
|
|
19
22
|
from pathlib import Path
|
|
20
23
|
from typing import Dict, List, Optional, Tuple, Any
|
|
@@ -2236,3 +2239,145 @@ def rename_workspace_path(path: str, new_path: str) -> Dict[str, Any]:
|
|
|
2236
2239
|
'previous_path': str(src),
|
|
2237
2240
|
'message': 'Path renamed successfully',
|
|
2238
2241
|
}
|
|
2242
|
+
|
|
2243
|
+
|
|
2244
|
+
def search_repository(
|
|
2245
|
+
repo_path: str,
|
|
2246
|
+
query: str,
|
|
2247
|
+
*,
|
|
2248
|
+
case_sensitive: bool = False,
|
|
2249
|
+
whole_word: bool = False,
|
|
2250
|
+
regex: bool = False,
|
|
2251
|
+
max_matches: int = 1000,
|
|
2252
|
+
max_count_per_file: int = 100,
|
|
2253
|
+
timeout_seconds: float = 30.0,
|
|
2254
|
+
) -> Dict[str, Any]:
|
|
2255
|
+
"""Search the working tree for ``query`` using ripgrep.
|
|
2256
|
+
|
|
2257
|
+
Honors ``.gitignore``, ``.git/info/exclude`` and ``.ignore`` (ripgrep defaults).
|
|
2258
|
+
Returns matches relative to ``repo_path``.
|
|
2259
|
+
|
|
2260
|
+
Raises ``RuntimeError`` with a structured prefix on failure:
|
|
2261
|
+
- ``RIPGREP_NOT_INSTALLED: <hint>`` — ``rg`` binary missing on this machine.
|
|
2262
|
+
- ``INVALID_PATTERN: <stderr line>`` — ripgrep rejected the regex.
|
|
2263
|
+
"""
|
|
2264
|
+
if not query:
|
|
2265
|
+
return {'matches': [], 'truncated': False, 'elapsed_ms': 0}
|
|
2266
|
+
|
|
2267
|
+
rg_path = shutil.which('rg')
|
|
2268
|
+
if rg_path is None:
|
|
2269
|
+
raise RuntimeError(
|
|
2270
|
+
'RIPGREP_NOT_INSTALLED: ripgrep (rg) is not installed on the client machine. '
|
|
2271
|
+
'Install it (e.g. `apt install ripgrep` or `brew install ripgrep`) and retry.'
|
|
2272
|
+
)
|
|
2273
|
+
|
|
2274
|
+
normalized_repo = _normalize_path(repo_path)
|
|
2275
|
+
if not Path(normalized_repo).is_dir():
|
|
2276
|
+
raise FileNotFoundError(f'Repository path not found: {repo_path}')
|
|
2277
|
+
|
|
2278
|
+
argv: List[str] = [
|
|
2279
|
+
rg_path,
|
|
2280
|
+
'--json',
|
|
2281
|
+
'--hidden',
|
|
2282
|
+
'--max-filesize=1M',
|
|
2283
|
+
'--crlf',
|
|
2284
|
+
f'--max-count={int(max_count_per_file)}',
|
|
2285
|
+
]
|
|
2286
|
+
if not regex:
|
|
2287
|
+
argv.append('-F') # fixed string
|
|
2288
|
+
if not case_sensitive:
|
|
2289
|
+
argv.append('-i')
|
|
2290
|
+
if whole_word:
|
|
2291
|
+
argv.append('-w')
|
|
2292
|
+
argv.append('--')
|
|
2293
|
+
argv.append(query)
|
|
2294
|
+
argv.append(str(normalized_repo))
|
|
2295
|
+
|
|
2296
|
+
start = time.monotonic()
|
|
2297
|
+
try:
|
|
2298
|
+
proc = subprocess.run(
|
|
2299
|
+
argv,
|
|
2300
|
+
capture_output=True,
|
|
2301
|
+
text=True,
|
|
2302
|
+
timeout=timeout_seconds,
|
|
2303
|
+
check=False,
|
|
2304
|
+
)
|
|
2305
|
+
except subprocess.TimeoutExpired:
|
|
2306
|
+
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
2307
|
+
return {'matches': [], 'truncated': True, 'elapsed_ms': elapsed_ms}
|
|
2308
|
+
|
|
2309
|
+
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
2310
|
+
|
|
2311
|
+
# ripgrep exit codes: 0 = matches, 1 = no matches, 2 = error.
|
|
2312
|
+
if proc.returncode == 2:
|
|
2313
|
+
stderr = (proc.stderr or '').strip().splitlines()
|
|
2314
|
+
first = stderr[0] if stderr else 'ripgrep failed'
|
|
2315
|
+
raise RuntimeError(f'INVALID_PATTERN: {first}')
|
|
2316
|
+
|
|
2317
|
+
matches: List[Dict[str, Any]] = []
|
|
2318
|
+
truncated = False
|
|
2319
|
+
repo_root = str(Path(normalized_repo).resolve())
|
|
2320
|
+
|
|
2321
|
+
for raw_line in (proc.stdout or '').splitlines():
|
|
2322
|
+
if not raw_line:
|
|
2323
|
+
continue
|
|
2324
|
+
try:
|
|
2325
|
+
event = json.loads(raw_line)
|
|
2326
|
+
except json.JSONDecodeError:
|
|
2327
|
+
continue
|
|
2328
|
+
if event.get('type') != 'match':
|
|
2329
|
+
continue
|
|
2330
|
+
data = event.get('data') or {}
|
|
2331
|
+
path_obj = (data.get('path') or {})
|
|
2332
|
+
file_path = path_obj.get('text')
|
|
2333
|
+
if not file_path:
|
|
2334
|
+
# Binary or non-utf8 path — skip.
|
|
2335
|
+
continue
|
|
2336
|
+
try:
|
|
2337
|
+
rel_path = str(Path(file_path).resolve().relative_to(repo_root))
|
|
2338
|
+
except ValueError:
|
|
2339
|
+
rel_path = file_path
|
|
2340
|
+
|
|
2341
|
+
line_obj = (data.get('lines') or {})
|
|
2342
|
+
preview_text = line_obj.get('text', '')
|
|
2343
|
+
if preview_text.endswith('\n'):
|
|
2344
|
+
preview_text = preview_text[:-1]
|
|
2345
|
+
if preview_text.endswith('\r'):
|
|
2346
|
+
preview_text = preview_text[:-1]
|
|
2347
|
+
# Cap preview length to keep payloads small.
|
|
2348
|
+
if len(preview_text) > 500:
|
|
2349
|
+
preview_text = preview_text[:500] + '…'
|
|
2350
|
+
|
|
2351
|
+
line_number = int(data.get('line_number') or 0)
|
|
2352
|
+
submatches = data.get('submatches') or []
|
|
2353
|
+
|
|
2354
|
+
if not submatches:
|
|
2355
|
+
continue
|
|
2356
|
+
|
|
2357
|
+
for submatch in submatches:
|
|
2358
|
+
start_off = int(submatch.get('start') or 0)
|
|
2359
|
+
end_off = int(submatch.get('end') or start_off)
|
|
2360
|
+
# Clamp to preview length (we may have truncated above).
|
|
2361
|
+
if start_off > len(preview_text):
|
|
2362
|
+
start_off = len(preview_text)
|
|
2363
|
+
if end_off > len(preview_text):
|
|
2364
|
+
end_off = len(preview_text)
|
|
2365
|
+
matches.append({
|
|
2366
|
+
'file': rel_path,
|
|
2367
|
+
'line': line_number,
|
|
2368
|
+
'col': start_off + 1,
|
|
2369
|
+
'preview': preview_text,
|
|
2370
|
+
'match_start': start_off,
|
|
2371
|
+
'match_end': end_off,
|
|
2372
|
+
})
|
|
2373
|
+
if len(matches) >= max_matches:
|
|
2374
|
+
truncated = True
|
|
2375
|
+
break
|
|
2376
|
+
if truncated:
|
|
2377
|
+
break
|
|
2378
|
+
|
|
2379
|
+
return {
|
|
2380
|
+
'matches': matches,
|
|
2381
|
+
'truncated': truncated,
|
|
2382
|
+
'elapsed_ms': elapsed_ms,
|
|
2383
|
+
}
|