bamboo-coding 0.1.8__tar.gz → 0.2.0__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.8 → bamboo_coding-0.2.0}/PKG-INFO +7 -2
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/pyproject.toml +9 -2
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/detection.py +5 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/runner.py +6 -1
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/capabilities/repo.py +3 -0
- bamboo_coding-0.2.0/src/bamboo_coding/chat/pty_session.py +15 -0
- bamboo_coding-0.2.0/src/bamboo_coding/chat/pty_session_win.py +125 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/main.py +41 -42
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/terminal_runtime.py +21 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding.egg-info/PKG-INFO +7 -2
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding.egg-info/SOURCES.txt +2 -0
- bamboo_coding-0.2.0/src/bamboo_coding.egg-info/requires.txt +9 -0
- bamboo_coding-0.1.8/src/bamboo_coding.egg-info/requires.txt +0 -5
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/README.md +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/setup.cfg +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/__init__.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/__init__.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/builtin_skills/__init__.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/builtin_skills/skill-create/SKILL.md +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/builtin_skills/skill-optimize/SKILL.md +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/compose.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/defs.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/runtime.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/sessions.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/skills.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/skills_install.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/store.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/streams.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/tasks.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/types.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/capabilities/__init__.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/__init__.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/__main__.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/adapter.py +0 -0
- /bamboo_coding-0.1.8/src/bamboo_coding/chat/pty_session.py → /bamboo_coding-0.2.0/src/bamboo_coding/chat/pty_session_unix.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/runtime.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/session.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/types.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/virtual_screen.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/config.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/controller_client.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/git_utils.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/journal.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/protocol.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/scrollback_buffer.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/status.py +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding.egg-info/dependency_links.txt +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding.egg-info/entry_points.txt +0 -0
- {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding.egg-info/top_level.txt +0 -0
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bamboo-coding
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Bamboo Coding outbound client
|
|
5
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
6
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
7
|
+
Classifier: Operating System :: MacOS
|
|
5
8
|
Requires-Python: >=3.12
|
|
6
9
|
Description-Content-Type: text/markdown
|
|
7
10
|
Requires-Dist: gitpython==3.1.40
|
|
8
11
|
Requires-Dist: websockets==13.1
|
|
9
12
|
Requires-Dist: pydantic>=2.0
|
|
10
|
-
Requires-Dist: bamboo-ssh
|
|
13
|
+
Requires-Dist: bamboo-ssh>=0.2.0
|
|
11
14
|
Requires-Dist: pyte>=0.8.2
|
|
15
|
+
Requires-Dist: psutil>=5.9
|
|
16
|
+
Requires-Dist: pywinpty>=2.0; sys_platform == "win32"
|
|
12
17
|
|
|
13
18
|
# bamboo-coding
|
|
14
19
|
|
|
@@ -4,16 +4,23 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bamboo-coding"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Bamboo Coding outbound client"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Operating System :: POSIX :: Linux",
|
|
13
|
+
"Operating System :: Microsoft :: Windows",
|
|
14
|
+
"Operating System :: MacOS",
|
|
15
|
+
]
|
|
11
16
|
dependencies = [
|
|
12
17
|
"gitpython==3.1.40",
|
|
13
18
|
"websockets==13.1",
|
|
14
19
|
"pydantic>=2.0",
|
|
15
|
-
"bamboo-ssh",
|
|
20
|
+
"bamboo-ssh>=0.2.0",
|
|
16
21
|
"pyte>=0.8.2",
|
|
22
|
+
"psutil>=5.9",
|
|
23
|
+
"pywinpty>=2.0; sys_platform == 'win32'",
|
|
17
24
|
]
|
|
18
25
|
|
|
19
26
|
[project.scripts]
|
|
@@ -4,6 +4,7 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import shutil
|
|
6
6
|
import subprocess
|
|
7
|
+
import sys
|
|
7
8
|
import threading
|
|
8
9
|
import time
|
|
9
10
|
from concurrent.futures import ThreadPoolExecutor
|
|
@@ -31,6 +32,10 @@ def resolve_bin(def_: AgentDef) -> str | None:
|
|
|
31
32
|
"""
|
|
32
33
|
override = os.environ.get(f"BAMBOO_CODING_{def_.id.upper()}_BIN")
|
|
33
34
|
if override:
|
|
35
|
+
if sys.platform == "win32":
|
|
36
|
+
# CreateProcess won't resolve `.cmd`/`.exe`-less names; which()
|
|
37
|
+
# honors PATHEXT and passes absolute paths through.
|
|
38
|
+
return shutil.which(override)
|
|
34
39
|
return override if os.access(override, os.X_OK) else None
|
|
35
40
|
for candidate in (def_.bin, *def_.fallback_bins):
|
|
36
41
|
path = shutil.which(candidate)
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
|
+
import shutil
|
|
6
7
|
|
|
7
8
|
from .streams import get_parser
|
|
8
9
|
from .types import DetectedAgent, RuntimeContext
|
|
@@ -62,7 +63,11 @@ class AgentProcess:
|
|
|
62
63
|
def build_argv(self) -> list[str]:
|
|
63
64
|
def_ = self.detected.def_
|
|
64
65
|
args = def_.build_args(self.model, self.extra_dirs, self.detected.caps, self.ctx)
|
|
65
|
-
|
|
66
|
+
# Resolve right before the spawn: on Windows, CreateProcess does not
|
|
67
|
+
# resolve npm `.cmd` shims by itself; which() honors PATHEXT. On
|
|
68
|
+
# POSIX, which() on the already-absolute detected path is a no-op.
|
|
69
|
+
binary = shutil.which(self.detected.path) or self.detected.path
|
|
70
|
+
return [binary, *args]
|
|
66
71
|
|
|
67
72
|
async def cancel(self) -> None:
|
|
68
73
|
"""SIGTERM the CLI (it flushes and exits); escalate to SIGKILL after a
|
|
@@ -13,6 +13,9 @@ def _allowed_roots(repositories: dict[str, str] | list[str] | tuple[str, ...]) -
|
|
|
13
13
|
values = list(repositories.values()) or list(repositories.keys())
|
|
14
14
|
else:
|
|
15
15
|
values = list(repositories)
|
|
16
|
+
# Guarantee: paths are resolved here, on the client that owns the
|
|
17
|
+
# filesystem. The server treats client paths as opaque, platform-tagged
|
|
18
|
+
# strings and never resolves them itself.
|
|
16
19
|
return [str(Path(item).expanduser().resolve()) for item in values]
|
|
17
20
|
|
|
18
21
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Platform switch for ChatPtySession.
|
|
2
|
+
|
|
3
|
+
On Windows the ConPTY-backed implementation is used; everywhere else the
|
|
4
|
+
original Unix PTY implementation is imported unchanged (``pty_session_unix``
|
|
5
|
+
is the old ``pty_session.py``, renamed without edits).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
if sys.platform == "win32":
|
|
11
|
+
from .pty_session_win import ChatPtySession
|
|
12
|
+
else:
|
|
13
|
+
from .pty_session_unix import ChatPtySession
|
|
14
|
+
|
|
15
|
+
__all__ = ["ChatPtySession"]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""ConPTY-backed ChatPtySession for native Windows.
|
|
2
|
+
|
|
3
|
+
Mirrors the shape of the Unix implementation (``master_fd``, ``process``,
|
|
4
|
+
``closed``, ``start``/``write``/``resize``/``close``) so it composes with
|
|
5
|
+
``bamboo_ssh.stream_terminal_output`` (whose Windows backend reads from the
|
|
6
|
+
pywinpty handle stored in ``master_fd``).
|
|
7
|
+
|
|
8
|
+
Unlike the shell-oriented ``bamboo_ssh`` session, this spawns an arbitrary
|
|
9
|
+
argv directly. Windows specifics:
|
|
10
|
+
|
|
11
|
+
- npm-installed CLIs are ``.cmd`` shims that ``CreateProcess`` does not
|
|
12
|
+
resolve by itself, so argv[0] is resolved with ``shutil.which`` (which
|
|
13
|
+
honors ``PATHEXT``) before spawning.
|
|
14
|
+
- There are no process groups; ``close()`` terminates the process and then
|
|
15
|
+
uses ``taskkill /T /F`` to reap the whole child tree — the Windows
|
|
16
|
+
replacement for ``os.killpg``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import contextlib
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
import time
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
_CLOSE_WAIT_SECONDS = 1.0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _ProcessAdapter:
|
|
33
|
+
"""subprocess.Popen-ish view over a pywinpty PtyProcess."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, pty_process: Any):
|
|
36
|
+
self._pty = pty_process
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def pid(self) -> int:
|
|
40
|
+
return self._pty.pid
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def returncode(self) -> int | None:
|
|
44
|
+
if self._pty.isalive():
|
|
45
|
+
return None
|
|
46
|
+
return self._pty.exitstatus
|
|
47
|
+
|
|
48
|
+
def poll(self) -> int | None:
|
|
49
|
+
return self.returncode
|
|
50
|
+
|
|
51
|
+
def wait(self, timeout: float | None = None) -> int | None:
|
|
52
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
53
|
+
while self._pty.isalive():
|
|
54
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
55
|
+
return None
|
|
56
|
+
time.sleep(0.05)
|
|
57
|
+
return self._pty.exitstatus
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ChatPtySession:
|
|
62
|
+
# Named ``master_fd`` for interface compatibility with the unix backend;
|
|
63
|
+
# holds the pywinpty PtyProcess handle rather than a file descriptor.
|
|
64
|
+
master_fd: Any
|
|
65
|
+
process: _ProcessAdapter
|
|
66
|
+
closed: bool = False
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def start(
|
|
70
|
+
cls,
|
|
71
|
+
*,
|
|
72
|
+
argv: list[str],
|
|
73
|
+
cwd: str | None = None,
|
|
74
|
+
cols: int = 120,
|
|
75
|
+
rows: int = 32,
|
|
76
|
+
env: dict[str, str] | None = None,
|
|
77
|
+
) -> "ChatPtySession":
|
|
78
|
+
from winpty import PtyProcess # pywinpty; Windows-only
|
|
79
|
+
|
|
80
|
+
if not argv:
|
|
81
|
+
raise ValueError("argv must not be empty")
|
|
82
|
+
merged_env = os.environ.copy()
|
|
83
|
+
merged_env.setdefault("TERM", "xterm-256color")
|
|
84
|
+
if env:
|
|
85
|
+
merged_env.update(env)
|
|
86
|
+
resolved = shutil.which(argv[0]) or argv[0]
|
|
87
|
+
pty_process = PtyProcess.spawn(
|
|
88
|
+
[resolved, *argv[1:]],
|
|
89
|
+
cwd=cwd or None,
|
|
90
|
+
env=merged_env,
|
|
91
|
+
dimensions=(rows, cols),
|
|
92
|
+
)
|
|
93
|
+
return cls(master_fd=pty_process, process=_ProcessAdapter(pty_process))
|
|
94
|
+
|
|
95
|
+
def write(self, data: str | bytes) -> None:
|
|
96
|
+
if self.closed:
|
|
97
|
+
return
|
|
98
|
+
payload = data.decode("utf-8", errors="replace") if isinstance(data, bytes) else data
|
|
99
|
+
self.master_fd.write(payload)
|
|
100
|
+
|
|
101
|
+
def resize(self, cols: int, rows: int) -> None:
|
|
102
|
+
if self.closed:
|
|
103
|
+
return
|
|
104
|
+
self.master_fd.setwinsize(rows, cols)
|
|
105
|
+
|
|
106
|
+
def close(self) -> None:
|
|
107
|
+
if self.closed:
|
|
108
|
+
return
|
|
109
|
+
self.closed = True
|
|
110
|
+
pid = self.master_fd.pid
|
|
111
|
+
with contextlib.suppress(Exception):
|
|
112
|
+
self.master_fd.terminate()
|
|
113
|
+
deadline = time.monotonic() + _CLOSE_WAIT_SECONDS
|
|
114
|
+
while self.master_fd.isalive() and time.monotonic() < deadline:
|
|
115
|
+
time.sleep(0.05)
|
|
116
|
+
if self.master_fd.isalive():
|
|
117
|
+
# Kill the whole child tree; grandchildren survive terminate().
|
|
118
|
+
with contextlib.suppress(Exception):
|
|
119
|
+
subprocess.run(
|
|
120
|
+
["taskkill", "/T", "/F", "/PID", str(pid)],
|
|
121
|
+
capture_output=True,
|
|
122
|
+
check=False,
|
|
123
|
+
)
|
|
124
|
+
with contextlib.suppress(Exception):
|
|
125
|
+
del self.master_fd.pty
|
|
@@ -5,11 +5,14 @@ import asyncio
|
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
7
|
import os
|
|
8
|
+
import platform as _platform
|
|
8
9
|
import random
|
|
9
10
|
import re
|
|
10
11
|
import signal
|
|
11
12
|
import sys
|
|
12
13
|
import time
|
|
14
|
+
|
|
15
|
+
import psutil
|
|
13
16
|
from dataclasses import asdict
|
|
14
17
|
from pathlib import Path
|
|
15
18
|
from typing import Awaitable, Callable
|
|
@@ -222,7 +225,7 @@ class AgentRuntime:
|
|
|
222
225
|
client_id=self.config.client_id,
|
|
223
226
|
hostname=self.config.hostname,
|
|
224
227
|
version=self.config.version,
|
|
225
|
-
platform_name="
|
|
228
|
+
platform_name=_platform.system().lower(), # "linux" / "windows" / "darwin"
|
|
226
229
|
capabilities=self.capabilities(),
|
|
227
230
|
roots=self.roots(),
|
|
228
231
|
active_tasks=active,
|
|
@@ -567,7 +570,7 @@ class AgentRuntime:
|
|
|
567
570
|
client_id=self.config.client_id,
|
|
568
571
|
hostname=self.config.hostname,
|
|
569
572
|
version=self.config.version,
|
|
570
|
-
platform_name="
|
|
573
|
+
platform_name=_platform.system().lower(), # "linux" / "windows" / "darwin"
|
|
571
574
|
reason="task_update",
|
|
572
575
|
capabilities=self.capabilities(),
|
|
573
576
|
roots=self.roots(),
|
|
@@ -721,6 +724,9 @@ def _prompt(label: str, default: str) -> str:
|
|
|
721
724
|
def _prompt_roots(default_roots: list[str]) -> list[str]:
|
|
722
725
|
default_text = ", ".join(default_roots)
|
|
723
726
|
raw = _prompt("Allowed roots (comma-separated)", default_text)
|
|
727
|
+
# Guarantee: roots leave the client absolute and resolved. The server
|
|
728
|
+
# treats client paths as opaque, platform-tagged strings and never
|
|
729
|
+
# resolves them itself.
|
|
724
730
|
return [
|
|
725
731
|
str(Path(item.strip()).expanduser().resolve())
|
|
726
732
|
for item in raw.split(",")
|
|
@@ -775,6 +781,9 @@ def setup_command(args: argparse.Namespace) -> int:
|
|
|
775
781
|
)
|
|
776
782
|
return 1
|
|
777
783
|
|
|
784
|
+
# Guarantee: roots leave the client absolute and resolved. The server
|
|
785
|
+
# treats client paths as opaque, platform-tagged strings and never
|
|
786
|
+
# resolves them itself.
|
|
778
787
|
default_roots = [
|
|
779
788
|
str(Path(root).expanduser().resolve())
|
|
780
789
|
for root in (args.roots or list(config.allowed_roots) or [str(Path.cwd().resolve())])
|
|
@@ -866,7 +875,10 @@ async def _run_runtime(runtime: AgentRuntime, stop_event: asyncio.Event | None =
|
|
|
866
875
|
registered_signals: list[signal.Signals] = []
|
|
867
876
|
if owns_signal_handlers:
|
|
868
877
|
loop = asyncio.get_running_loop()
|
|
869
|
-
|
|
878
|
+
_signals = [signal.SIGINT, signal.SIGTERM]
|
|
879
|
+
if hasattr(signal, "SIGHUP"): # not on Windows
|
|
880
|
+
_signals.append(signal.SIGHUP)
|
|
881
|
+
for signum in _signals:
|
|
870
882
|
try:
|
|
871
883
|
loop.add_signal_handler(signum, stop_event.set)
|
|
872
884
|
registered_signals.append(signum)
|
|
@@ -972,60 +984,45 @@ def _is_bamboo_client(argv: list[str]) -> bool:
|
|
|
972
984
|
|
|
973
985
|
|
|
974
986
|
def _find_running_clients() -> list[tuple[int, list[str]]]:
|
|
975
|
-
"""
|
|
987
|
+
"""Find running bamboo-coding client processes via psutil (cross-platform)."""
|
|
976
988
|
results: list[tuple[int, list[str]]] = []
|
|
977
|
-
proc_dir = Path("/proc")
|
|
978
|
-
if not proc_dir.exists():
|
|
979
|
-
return results
|
|
980
989
|
self_pid = os.getpid()
|
|
981
|
-
for
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
pid = int(entry.name)
|
|
985
|
-
if pid == self_pid:
|
|
986
|
-
continue
|
|
987
|
-
try:
|
|
988
|
-
raw = (entry / "cmdline").read_bytes()
|
|
989
|
-
except (FileNotFoundError, PermissionError, OSError):
|
|
990
|
-
continue
|
|
991
|
-
if not raw:
|
|
990
|
+
for proc in psutil.process_iter(["pid", "cmdline"]):
|
|
991
|
+
pid = proc.info.get("pid")
|
|
992
|
+
if pid is None or pid == self_pid:
|
|
992
993
|
continue
|
|
993
|
-
argv =
|
|
994
|
-
if _is_bamboo_client(argv):
|
|
994
|
+
argv = proc.info.get("cmdline") or []
|
|
995
|
+
if argv and _is_bamboo_client(argv):
|
|
995
996
|
results.append((pid, argv))
|
|
996
997
|
return results
|
|
997
998
|
|
|
998
999
|
|
|
999
1000
|
def _pid_alive(pid: int) -> bool:
|
|
1000
1001
|
try:
|
|
1001
|
-
|
|
1002
|
-
|
|
1002
|
+
# A zombie has already exited (its parent just hasn't reaped it) —
|
|
1003
|
+
# counting it alive would make `stop` report a false failure.
|
|
1004
|
+
return psutil.Process(pid).status() != psutil.STATUS_ZOMBIE
|
|
1005
|
+
except psutil.NoSuchProcess:
|
|
1003
1006
|
return False
|
|
1004
|
-
except
|
|
1007
|
+
except psutil.AccessDenied:
|
|
1005
1008
|
return True
|
|
1006
|
-
|
|
1009
|
+
except Exception:
|
|
1010
|
+
return False
|
|
1007
1011
|
|
|
1008
1012
|
|
|
1009
1013
|
def _lookup_client_argv(pid: int) -> list[str] | None:
|
|
1010
1014
|
"""Return argv for `pid` if it is a bamboo-coding client process, else None."""
|
|
1011
1015
|
try:
|
|
1012
|
-
|
|
1013
|
-
except (
|
|
1014
|
-
return None
|
|
1015
|
-
if not raw:
|
|
1016
|
+
argv = psutil.Process(pid).cmdline()
|
|
1017
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess, OSError):
|
|
1016
1018
|
return None
|
|
1017
|
-
|
|
1018
|
-
if not _is_bamboo_client(argv):
|
|
1019
|
+
if not argv or not _is_bamboo_client(argv):
|
|
1019
1020
|
return None
|
|
1020
1021
|
return argv
|
|
1021
1022
|
|
|
1022
1023
|
|
|
1023
1024
|
def status_command(args: argparse.Namespace) -> int:
|
|
1024
1025
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
|
1025
|
-
if sys.platform != "linux":
|
|
1026
|
-
print("'bamboo-coding status' is only supported on Linux (it reads /proc).", file=sys.stderr)
|
|
1027
|
-
return 2
|
|
1028
|
-
|
|
1029
1026
|
clients = _find_running_clients()
|
|
1030
1027
|
if not clients:
|
|
1031
1028
|
print("No running bamboo-coding clients found.")
|
|
@@ -1039,10 +1036,6 @@ def status_command(args: argparse.Namespace) -> int:
|
|
|
1039
1036
|
|
|
1040
1037
|
def stop_command(args: argparse.Namespace) -> int:
|
|
1041
1038
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
|
1042
|
-
if sys.platform != "linux":
|
|
1043
|
-
print("'bamboo-coding stop' is only supported on Linux (it reads /proc).", file=sys.stderr)
|
|
1044
|
-
return 2
|
|
1045
|
-
|
|
1046
1039
|
requested_pids: list[int] = list(getattr(args, "pids", []) or [])
|
|
1047
1040
|
|
|
1048
1041
|
if requested_pids:
|
|
@@ -1071,17 +1064,23 @@ def stop_command(args: argparse.Namespace) -> int:
|
|
|
1071
1064
|
for pid, argv in clients:
|
|
1072
1065
|
print(f" PID {pid}: {' '.join(argv)}")
|
|
1073
1066
|
|
|
1074
|
-
|
|
1067
|
+
# psutil kill()/terminate() replace SIGKILL/SIGTERM: SIGKILL does not
|
|
1068
|
+
# exist on Windows, and psutil maps both calls to the right primitive
|
|
1069
|
+
# on every OS.
|
|
1075
1070
|
sig_name = "SIGKILL" if args.force else "SIGTERM"
|
|
1076
1071
|
sent: list[int] = []
|
|
1077
1072
|
for pid, _ in clients:
|
|
1078
1073
|
try:
|
|
1079
|
-
|
|
1074
|
+
proc = psutil.Process(pid)
|
|
1075
|
+
if args.force:
|
|
1076
|
+
proc.kill()
|
|
1077
|
+
else:
|
|
1078
|
+
proc.terminate()
|
|
1080
1079
|
sent.append(pid)
|
|
1081
1080
|
print(f"Sent {sig_name} to PID {pid}")
|
|
1082
|
-
except
|
|
1081
|
+
except psutil.NoSuchProcess:
|
|
1083
1082
|
print(f"PID {pid} already exited")
|
|
1084
|
-
except
|
|
1083
|
+
except psutil.AccessDenied:
|
|
1085
1084
|
print(f"Permission denied for PID {pid}; rerun with sudo")
|
|
1086
1085
|
|
|
1087
1086
|
if not sent or args.force:
|
|
@@ -5,6 +5,7 @@ import inspect
|
|
|
5
5
|
import os
|
|
6
6
|
import shlex
|
|
7
7
|
import signal as _signal
|
|
8
|
+
import sys
|
|
8
9
|
import time
|
|
9
10
|
from dataclasses import dataclass, field
|
|
10
11
|
from pathlib import Path
|
|
@@ -43,6 +44,11 @@ def build_initial_cwd_command(path: str) -> str:
|
|
|
43
44
|
target = Path(path).expanduser()
|
|
44
45
|
resolved = target.resolve(strict=False)
|
|
45
46
|
working_dir = resolved if resolved.is_dir() else resolved.parent
|
|
47
|
+
if sys.platform == "win32":
|
|
48
|
+
# A `cd "..."` both PowerShell and cmd accept; embedded quotes are
|
|
49
|
+
# doubled, and the line ending is \r\n.
|
|
50
|
+
escaped = str(working_dir).replace('"', '""')
|
|
51
|
+
return f'cd "{escaped}"\r\n'
|
|
46
52
|
return f"cd {shlex.quote(str(working_dir))}\n"
|
|
47
53
|
|
|
48
54
|
|
|
@@ -210,6 +216,21 @@ class ClientTerminalRuntime:
|
|
|
210
216
|
active = self.sessions.get(message.terminal_id)
|
|
211
217
|
if active is None:
|
|
212
218
|
return
|
|
219
|
+
if sys.platform == "win32":
|
|
220
|
+
# No process groups or tcgetpgrp on Windows. Ctrl+C travels
|
|
221
|
+
# through the ConPTY input stream; SIGTERM closes the session
|
|
222
|
+
# (the win backend tears down the console tree itself).
|
|
223
|
+
if message.signal == "SIGINT":
|
|
224
|
+
try:
|
|
225
|
+
active.session.write("\x03")
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
await self._emit(
|
|
228
|
+
self.send_status,
|
|
229
|
+
TerminalStatusMessage(terminal_id=message.terminal_id, status='error', message=str(exc)),
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
await self.close_terminal(message.terminal_id, reason='signalled')
|
|
233
|
+
return
|
|
213
234
|
master_fd = active.session.master_fd
|
|
214
235
|
try:
|
|
215
236
|
pgid = os.tcgetpgrp(master_fd)
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bamboo-coding
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Bamboo Coding outbound client
|
|
5
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
6
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
7
|
+
Classifier: Operating System :: MacOS
|
|
5
8
|
Requires-Python: >=3.12
|
|
6
9
|
Description-Content-Type: text/markdown
|
|
7
10
|
Requires-Dist: gitpython==3.1.40
|
|
8
11
|
Requires-Dist: websockets==13.1
|
|
9
12
|
Requires-Dist: pydantic>=2.0
|
|
10
|
-
Requires-Dist: bamboo-ssh
|
|
13
|
+
Requires-Dist: bamboo-ssh>=0.2.0
|
|
11
14
|
Requires-Dist: pyte>=0.8.2
|
|
15
|
+
Requires-Dist: psutil>=5.9
|
|
16
|
+
Requires-Dist: pywinpty>=2.0; sys_platform == "win32"
|
|
12
17
|
|
|
13
18
|
# bamboo-coding
|
|
14
19
|
|
|
@@ -38,6 +38,8 @@ src/bamboo_coding/chat/__init__.py
|
|
|
38
38
|
src/bamboo_coding/chat/__main__.py
|
|
39
39
|
src/bamboo_coding/chat/adapter.py
|
|
40
40
|
src/bamboo_coding/chat/pty_session.py
|
|
41
|
+
src/bamboo_coding/chat/pty_session_unix.py
|
|
42
|
+
src/bamboo_coding/chat/pty_session_win.py
|
|
41
43
|
src/bamboo_coding/chat/runtime.py
|
|
42
44
|
src/bamboo_coding/chat/session.py
|
|
43
45
|
src/bamboo_coding/chat/types.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/builtin_skills/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|