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.
Files changed (49) hide show
  1. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/PKG-INFO +7 -2
  2. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/pyproject.toml +9 -2
  3. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/detection.py +5 -0
  4. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/runner.py +6 -1
  5. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/capabilities/repo.py +3 -0
  6. bamboo_coding-0.2.0/src/bamboo_coding/chat/pty_session.py +15 -0
  7. bamboo_coding-0.2.0/src/bamboo_coding/chat/pty_session_win.py +125 -0
  8. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/main.py +41 -42
  9. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/terminal_runtime.py +21 -0
  10. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding.egg-info/PKG-INFO +7 -2
  11. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding.egg-info/SOURCES.txt +2 -0
  12. bamboo_coding-0.2.0/src/bamboo_coding.egg-info/requires.txt +9 -0
  13. bamboo_coding-0.1.8/src/bamboo_coding.egg-info/requires.txt +0 -5
  14. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/README.md +0 -0
  15. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/setup.cfg +0 -0
  16. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/__init__.py +0 -0
  17. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/__init__.py +0 -0
  18. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/builtin_skills/__init__.py +0 -0
  19. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/builtin_skills/skill-create/SKILL.md +0 -0
  20. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/builtin_skills/skill-optimize/SKILL.md +0 -0
  21. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/compose.py +0 -0
  22. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/defs.py +0 -0
  23. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/runtime.py +0 -0
  24. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/sessions.py +0 -0
  25. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/skills.py +0 -0
  26. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/skills_install.py +0 -0
  27. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/store.py +0 -0
  28. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/streams.py +0 -0
  29. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/tasks.py +0 -0
  30. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/agents/types.py +0 -0
  31. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/capabilities/__init__.py +0 -0
  32. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/__init__.py +0 -0
  33. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/__main__.py +0 -0
  34. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/adapter.py +0 -0
  35. /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
  36. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/runtime.py +0 -0
  37. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/session.py +0 -0
  38. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/types.py +0 -0
  39. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/chat/virtual_screen.py +0 -0
  40. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/config.py +0 -0
  41. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/controller_client.py +0 -0
  42. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/git_utils.py +0 -0
  43. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/journal.py +0 -0
  44. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/protocol.py +0 -0
  45. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/scrollback_buffer.py +0 -0
  46. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding/status.py +0 -0
  47. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding.egg-info/dependency_links.txt +0 -0
  48. {bamboo_coding-0.1.8 → bamboo_coding-0.2.0}/src/bamboo_coding.egg-info/entry_points.txt +0 -0
  49. {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.1.8
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.1.8"
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
- return [self.detected.path, *args]
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="python",
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="python",
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
- for signum in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
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
- """Walk /proc to find running bamboo-coding client processes (Linux only)."""
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 entry in proc_dir.iterdir():
982
- if not entry.name.isdigit():
983
- continue
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 = [part.decode("utf-8", errors="replace") for part in raw.split(b"\x00") if part]
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
- os.kill(pid, 0)
1002
- except ProcessLookupError:
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 PermissionError:
1007
+ except psutil.AccessDenied:
1005
1008
  return True
1006
- return True
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
- raw = Path(f"/proc/{pid}/cmdline").read_bytes()
1013
- except (FileNotFoundError, PermissionError, OSError):
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
- argv = [part.decode("utf-8", errors="replace") for part in raw.split(b"\x00") if part]
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
- sig = signal.SIGKILL if args.force else signal.SIGTERM
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
- os.kill(pid, sig)
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 ProcessLookupError:
1081
+ except psutil.NoSuchProcess:
1083
1082
  print(f"PID {pid} already exited")
1084
- except PermissionError:
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.1.8
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
@@ -0,0 +1,9 @@
1
+ gitpython==3.1.40
2
+ websockets==13.1
3
+ pydantic>=2.0
4
+ bamboo-ssh>=0.2.0
5
+ pyte>=0.8.2
6
+ psutil>=5.9
7
+
8
+ [:sys_platform == "win32"]
9
+ pywinpty>=2.0
@@ -1,5 +0,0 @@
1
- gitpython==3.1.40
2
- websockets==13.1
3
- pydantic>=2.0
4
- bamboo-ssh
5
- pyte>=0.8.2
File without changes
File without changes