tabby-bianbu-mcp 0.8.0 → 0.8.2
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.
- package/CHANGELOG.md +15 -0
- package/assets/bianbu_agent_proxy.meta.json +3 -3
- package/assets/bianbu_agent_proxy.sh +97 -62
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `tabby-bianbu-mcp` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.8.2] - 2026-03-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **PTY helper SIGCHLD race condition**: replaced `SIGCHLD` signal handler with non-blocking `waitpid(WNOHANG)` poll — `.bashrc` subprocesses (e.g. `dircolors`, `lesspipe`) were triggering SIGCHLD, setting `running=False`, and killing the PTY before bash was ready
|
|
9
|
+
- **PTY helper stdin EOF exit**: stdin EOF no longer terminates the helper; instead it stops watching stdin and lets master_fd drain until bash actually exits
|
|
10
|
+
- **PTY stderr visibility**: Python helper stderr is now forwarded to the terminal output as red diagnostic text instead of being silently discarded
|
|
11
|
+
- **select() timeout tuned**: changed from 50ms to 500ms to reduce CPU usage during idle PTY sessions
|
|
12
|
+
|
|
13
|
+
## [0.8.1] - 2026-03-23
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **PTY shell tab closing immediately**: removed `closed.next()` from poll loop on process exit — prevents Tabby from auto-closing the tab when the PTY process ends
|
|
17
|
+
- **PTY startup fallback**: if `open_pty_session` fails (e.g. python3 unavailable), automatically falls back to `BianbuShellSession` instead of leaving a dead tab
|
|
18
|
+
- **Server-side early crash detection**: `open_pty_session` now waits 200ms and checks if the PTY child is still alive before returning, giving a clear error instead of a silently dead session
|
|
19
|
+
|
|
5
20
|
## [0.8.0] - 2026-03-23
|
|
6
21
|
|
|
7
22
|
### Added (Remote MCP Server v1.4.0)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"sourceFile": "bianbu_agent_proxy.sh",
|
|
4
4
|
"scriptVersion": "1.4.0",
|
|
5
5
|
"serverVersion": "1.4.0",
|
|
6
|
-
"sha256": "
|
|
7
|
-
"bytes":
|
|
8
|
-
"generatedAt": "2026-03-23T09:
|
|
6
|
+
"sha256": "273cfef8c453138dffd0a66e9e35ade821314e02c5247167f23a798f4d8c645d",
|
|
7
|
+
"bytes": 87190,
|
|
8
|
+
"generatedAt": "2026-03-23T09:53:28.729Z"
|
|
9
9
|
}
|
|
@@ -791,78 +791,94 @@ else:
|
|
|
791
791
|
stdin_fd = sys.stdin.fileno()
|
|
792
792
|
old_stdin_flags = fcntl.fcntl(stdin_fd, fcntl.F_GETFL)
|
|
793
793
|
fcntl.fcntl(stdin_fd, fcntl.F_SETFL, old_stdin_flags | os.O_NONBLOCK)
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
running = False
|
|
798
|
-
signal.signal(signal.SIGCHLD, on_sigchld)
|
|
794
|
+
# Ignore SIGCHLD to prevent select() interruption from .bashrc subprocesses etc.
|
|
795
|
+
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
|
|
796
|
+
child_exited = False
|
|
799
797
|
stdin_buf = b''
|
|
800
|
-
while
|
|
798
|
+
while True:
|
|
799
|
+
fds_to_watch = [master_fd]
|
|
800
|
+
if not child_exited:
|
|
801
|
+
fds_to_watch.append(stdin_fd)
|
|
801
802
|
try:
|
|
802
|
-
readable, _, _ = select.select(
|
|
803
|
+
readable, _, _ = select.select(fds_to_watch, [], [], 0.5)
|
|
803
804
|
except (select.error, InterruptedError, ValueError):
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
break
|
|
815
|
-
if stdin_fd in readable:
|
|
816
|
-
try:
|
|
817
|
-
chunk = os.read(stdin_fd, 65536)
|
|
818
|
-
if not chunk:
|
|
805
|
+
pass
|
|
806
|
+
else:
|
|
807
|
+
if master_fd in readable:
|
|
808
|
+
try:
|
|
809
|
+
data = os.read(master_fd, 65536)
|
|
810
|
+
if data:
|
|
811
|
+
send_msg({'type': 'output', 'data': base64.b64encode(data).decode('ascii')})
|
|
812
|
+
else:
|
|
813
|
+
break
|
|
814
|
+
except OSError:
|
|
819
815
|
break
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
os.write(master_fd, raw)
|
|
831
|
-
elif cmd['type'] == 'resize':
|
|
832
|
-
set_winsize(master_fd, cmd['rows'], cmd['cols'])
|
|
816
|
+
if stdin_fd in readable:
|
|
817
|
+
try:
|
|
818
|
+
chunk = os.read(stdin_fd, 65536)
|
|
819
|
+
if chunk:
|
|
820
|
+
stdin_buf += chunk
|
|
821
|
+
while b'\n' in stdin_buf:
|
|
822
|
+
line, stdin_buf = stdin_buf.split(b'\n', 1)
|
|
823
|
+
line = line.strip()
|
|
824
|
+
if not line:
|
|
825
|
+
continue
|
|
833
826
|
try:
|
|
834
|
-
|
|
835
|
-
|
|
827
|
+
cmd = json.loads(line.decode('utf-8'))
|
|
828
|
+
if cmd['type'] == 'input':
|
|
829
|
+
raw = base64.b64decode(cmd['data'])
|
|
830
|
+
os.write(master_fd, raw)
|
|
831
|
+
elif cmd['type'] == 'resize':
|
|
832
|
+
set_winsize(master_fd, cmd['rows'], cmd['cols'])
|
|
833
|
+
try:
|
|
834
|
+
os.kill(pid, signal.SIGWINCH)
|
|
835
|
+
except OSError:
|
|
836
|
+
pass
|
|
837
|
+
elif cmd['type'] == 'close':
|
|
838
|
+
child_exited = True
|
|
839
|
+
break
|
|
840
|
+
except (json.JSONDecodeError, KeyError, OSError):
|
|
836
841
|
pass
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
pass
|
|
842
|
-
except OSError:
|
|
843
|
-
break
|
|
844
|
-
try:
|
|
845
|
-
os.kill(pid, signal.SIGTERM)
|
|
846
|
-
except OSError:
|
|
847
|
-
pass
|
|
848
|
-
exit_code = -1
|
|
849
|
-
try:
|
|
850
|
-
for _ in range(50):
|
|
851
|
-
rpid, status = os.waitpid(pid, os.WNOHANG)
|
|
852
|
-
if rpid != 0:
|
|
853
|
-
exit_code = os.WEXITSTATUS(status) if os.WIFEXITED(status) else -1
|
|
854
|
-
break
|
|
855
|
-
import time; time.sleep(0.1)
|
|
856
|
-
else:
|
|
842
|
+
except OSError:
|
|
843
|
+
pass
|
|
844
|
+
# Check if child process has exited (non-blocking)
|
|
845
|
+
if not child_exited:
|
|
857
846
|
try:
|
|
858
|
-
os.
|
|
859
|
-
|
|
847
|
+
rpid, status = os.waitpid(pid, os.WNOHANG)
|
|
848
|
+
if rpid != 0:
|
|
849
|
+
child_exited = True
|
|
850
|
+
except ChildProcessError:
|
|
851
|
+
child_exited = True
|
|
852
|
+
# If child exited, drain remaining master_fd output then break
|
|
853
|
+
if child_exited:
|
|
854
|
+
import time
|
|
855
|
+
time.sleep(0.1)
|
|
856
|
+
try:
|
|
857
|
+
while True:
|
|
858
|
+
leftover = os.read(master_fd, 65536)
|
|
859
|
+
if not leftover:
|
|
860
|
+
break
|
|
861
|
+
send_msg({'type': 'output', 'data': base64.b64encode(leftover).decode('ascii')})
|
|
860
862
|
except OSError:
|
|
861
863
|
pass
|
|
864
|
+
break
|
|
865
|
+
exit_code = -1
|
|
866
|
+
if not child_exited:
|
|
867
|
+
try:
|
|
868
|
+
os.kill(pid, signal.SIGTERM)
|
|
869
|
+
except OSError:
|
|
870
|
+
pass
|
|
871
|
+
try:
|
|
872
|
+
rpid, status = os.waitpid(pid, os.WNOHANG if child_exited else 0)
|
|
873
|
+
if rpid != 0:
|
|
874
|
+
exit_code = os.WEXITSTATUS(status) if os.WIFEXITED(status) else -1
|
|
862
875
|
except ChildProcessError:
|
|
863
876
|
pass
|
|
864
877
|
send_msg({'type': 'exit', 'code': exit_code})
|
|
865
|
-
|
|
878
|
+
try:
|
|
879
|
+
os.close(master_fd)
|
|
880
|
+
except OSError:
|
|
881
|
+
pass
|
|
866
882
|
`;
|
|
867
883
|
}
|
|
868
884
|
|
|
@@ -934,7 +950,20 @@ function createPtySession(id, cwd, asRoot, cols, rows) {
|
|
|
934
950
|
} catch {}
|
|
935
951
|
}
|
|
936
952
|
});
|
|
937
|
-
child.stderr.on('data', () => {
|
|
953
|
+
child.stderr.on('data', (chunk) => {
|
|
954
|
+
const msg = chunk.toString().trim();
|
|
955
|
+
if (msg) {
|
|
956
|
+
// Inject stderr as a synthetic output line so the client can see errors
|
|
957
|
+
const buf = Buffer.from(`\r\n\x1b[31m[pty-helper stderr] ${msg}\x1b[0m\r\n`);
|
|
958
|
+
session.outputBuffer.push(buf);
|
|
959
|
+
session.outputBufferSize += buf.length;
|
|
960
|
+
session.updatedAt = Date.now();
|
|
961
|
+
for (const waiter of session.waiters) {
|
|
962
|
+
waiter();
|
|
963
|
+
}
|
|
964
|
+
session.waiters = [];
|
|
965
|
+
}
|
|
966
|
+
});
|
|
938
967
|
child.on('exit', () => {
|
|
939
968
|
session.alive = false;
|
|
940
969
|
session.updatedAt = Date.now();
|
|
@@ -1570,7 +1599,13 @@ function makeServer() {
|
|
|
1570
1599
|
}
|
|
1571
1600
|
await ensurePtyHelper();
|
|
1572
1601
|
const session_id = newSessionId('pty');
|
|
1573
|
-
createPtySession(session_id, workingDirectory, as_root, cols, rows);
|
|
1602
|
+
const session = createPtySession(session_id, workingDirectory, as_root, cols, rows);
|
|
1603
|
+
// Wait briefly to detect immediate crash
|
|
1604
|
+
await new Promise(r => setTimeout(r, 200));
|
|
1605
|
+
if (!session.alive) {
|
|
1606
|
+
ptySessions.delete(session_id);
|
|
1607
|
+
throw new Error('PTY session exited immediately — check that python3 is available and the working directory is accessible');
|
|
1608
|
+
}
|
|
1574
1609
|
const payload = { session_id, cwd: workingDirectory, as_root, cols, rows };
|
|
1575
1610
|
return textResult(JSON.stringify(payload, null, 2), payload);
|
|
1576
1611
|
},
|