@oneciel-ai/claude-any 0.1.95 → 0.1.97
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/claude_any.py +300 -8
- package/package.json +1 -1
package/claude_any.py
CHANGED
|
@@ -9,6 +9,7 @@ import importlib.util
|
|
|
9
9
|
import json
|
|
10
10
|
import math
|
|
11
11
|
import os
|
|
12
|
+
import queue
|
|
12
13
|
import re
|
|
13
14
|
import signal
|
|
14
15
|
import shlex
|
|
@@ -105,7 +106,7 @@ OFFICIAL_CHANNEL_PLUGINS = {
|
|
|
105
106
|
"fakechat": "plugin:fakechat@claude-plugins-official",
|
|
106
107
|
}
|
|
107
108
|
APP_NAME = "Claude Any"
|
|
108
|
-
VERSION = "0.1.
|
|
109
|
+
VERSION = "0.1.97"
|
|
109
110
|
CREDITS = "Credits: One Ciel LLC"
|
|
110
111
|
|
|
111
112
|
LOG_LEVELS = {"SILENT": 0, "ERROR": 1, "WARN": 2, "INFO": 3, "DEBUG": 4, "TRACE": 5}
|
|
@@ -9498,6 +9499,233 @@ def _mcp_server_is_stdio(server: dict[str, Any]) -> bool:
|
|
|
9498
9499
|
return "mcp-proxy" not in args
|
|
9499
9500
|
|
|
9500
9501
|
|
|
9502
|
+
def _channel_probe_initialize_payload() -> bytes:
|
|
9503
|
+
payload = {
|
|
9504
|
+
"jsonrpc": "2.0",
|
|
9505
|
+
"id": 1,
|
|
9506
|
+
"method": "initialize",
|
|
9507
|
+
"params": {
|
|
9508
|
+
"protocolVersion": "2024-11-05",
|
|
9509
|
+
"capabilities": {},
|
|
9510
|
+
"clientInfo": {"name": "claude-any-channel-probe", "version": VERSION},
|
|
9511
|
+
},
|
|
9512
|
+
}
|
|
9513
|
+
return json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
9514
|
+
|
|
9515
|
+
|
|
9516
|
+
def _channel_probe_parse_framed_responses(buffer: bytes) -> list[dict[str, Any]]:
|
|
9517
|
+
out: list[dict[str, Any]] = []
|
|
9518
|
+
idx = 0
|
|
9519
|
+
while idx < len(buffer):
|
|
9520
|
+
header_end = buffer.find(b"\r\n\r\n", idx)
|
|
9521
|
+
if header_end < 0:
|
|
9522
|
+
return out
|
|
9523
|
+
header = buffer[idx:header_end].decode("ascii", errors="replace")
|
|
9524
|
+
length: int | None = None
|
|
9525
|
+
for line in header.split("\r\n"):
|
|
9526
|
+
if line.lower().startswith("content-length:"):
|
|
9527
|
+
try:
|
|
9528
|
+
length = int(line.split(":", 1)[1].strip())
|
|
9529
|
+
except Exception:
|
|
9530
|
+
return out
|
|
9531
|
+
break
|
|
9532
|
+
if length is None:
|
|
9533
|
+
return out
|
|
9534
|
+
body_start = header_end + 4
|
|
9535
|
+
body_end = body_start + length
|
|
9536
|
+
if len(buffer) < body_end:
|
|
9537
|
+
return out
|
|
9538
|
+
try:
|
|
9539
|
+
msg = json.loads(buffer[body_start:body_end].decode("utf-8", errors="replace"))
|
|
9540
|
+
except Exception:
|
|
9541
|
+
idx = body_end
|
|
9542
|
+
continue
|
|
9543
|
+
if isinstance(msg, dict):
|
|
9544
|
+
out.append(msg)
|
|
9545
|
+
idx = body_end
|
|
9546
|
+
return out
|
|
9547
|
+
|
|
9548
|
+
|
|
9549
|
+
def _channel_probe_parse_jsonl_responses(buffer: bytes) -> list[dict[str, Any]]:
|
|
9550
|
+
out: list[dict[str, Any]] = []
|
|
9551
|
+
for raw_line in buffer.split(b"\n"):
|
|
9552
|
+
line = raw_line.strip()
|
|
9553
|
+
if not line:
|
|
9554
|
+
continue
|
|
9555
|
+
try:
|
|
9556
|
+
msg = json.loads(line.decode("utf-8", errors="replace"))
|
|
9557
|
+
except Exception:
|
|
9558
|
+
continue
|
|
9559
|
+
if isinstance(msg, dict):
|
|
9560
|
+
out.append(msg)
|
|
9561
|
+
return out
|
|
9562
|
+
|
|
9563
|
+
|
|
9564
|
+
def _channel_probe_find_initialize_response(buffer: bytes, framed: bool) -> dict[str, Any] | None:
|
|
9565
|
+
msgs = _channel_probe_parse_framed_responses(buffer) if framed else _channel_probe_parse_jsonl_responses(buffer)
|
|
9566
|
+
for msg in msgs:
|
|
9567
|
+
if msg.get("id") == 1 and "result" in msg:
|
|
9568
|
+
return msg
|
|
9569
|
+
return None
|
|
9570
|
+
|
|
9571
|
+
|
|
9572
|
+
def _channel_probe_capability_present(initialize_response: dict[str, Any]) -> bool:
|
|
9573
|
+
result = initialize_response.get("result")
|
|
9574
|
+
if not isinstance(result, dict):
|
|
9575
|
+
return False
|
|
9576
|
+
capabilities = result.get("capabilities")
|
|
9577
|
+
if not isinstance(capabilities, dict):
|
|
9578
|
+
return False
|
|
9579
|
+
experimental = capabilities.get("experimental")
|
|
9580
|
+
if not isinstance(experimental, dict):
|
|
9581
|
+
return False
|
|
9582
|
+
value = experimental.get("claude/channel")
|
|
9583
|
+
return value is not None and value is not False
|
|
9584
|
+
|
|
9585
|
+
|
|
9586
|
+
def probe_stdio_mcp_for_channel_capability(server_name: str, server: dict[str, Any], timeout: float = 3.0) -> bool:
|
|
9587
|
+
if not _mcp_server_is_stdio(server):
|
|
9588
|
+
return False
|
|
9589
|
+
command = str(server.get("command") or "").strip()
|
|
9590
|
+
args_raw = server.get("args", [])
|
|
9591
|
+
args = [str(item) for item in args_raw] if isinstance(args_raw, list) else []
|
|
9592
|
+
if not command:
|
|
9593
|
+
return False
|
|
9594
|
+
command, args = resolve_mcp_server_process(command, args)
|
|
9595
|
+
env = os.environ.copy()
|
|
9596
|
+
raw_env = server.get("env")
|
|
9597
|
+
if isinstance(raw_env, dict):
|
|
9598
|
+
env.update({str(k): str(v) for k, v in raw_env.items() if str(k)})
|
|
9599
|
+
cwd_value = server.get("cwd") or server.get("workingDirectory")
|
|
9600
|
+
cwd = str(cwd_value) if cwd_value else None
|
|
9601
|
+
framed = _mcp_proxy_stdio_mode(server) != "jsonl"
|
|
9602
|
+
|
|
9603
|
+
proc: subprocess.Popen[bytes] | None = None
|
|
9604
|
+
try:
|
|
9605
|
+
proc = subprocess.Popen(
|
|
9606
|
+
[command, *args],
|
|
9607
|
+
stdin=subprocess.PIPE,
|
|
9608
|
+
stdout=subprocess.PIPE,
|
|
9609
|
+
stderr=subprocess.DEVNULL,
|
|
9610
|
+
cwd=cwd,
|
|
9611
|
+
env=env,
|
|
9612
|
+
bufsize=0,
|
|
9613
|
+
close_fds=True,
|
|
9614
|
+
)
|
|
9615
|
+
except Exception as exc:
|
|
9616
|
+
router_log("DEBUG", f"channel_probe_spawn_failed server={server_name} error={type(exc).__name__}: {exc}")
|
|
9617
|
+
return False
|
|
9618
|
+
|
|
9619
|
+
chunks_queue: queue.Queue[bytes | None] = queue.Queue()
|
|
9620
|
+
|
|
9621
|
+
def _reader() -> None:
|
|
9622
|
+
try:
|
|
9623
|
+
assert proc is not None
|
|
9624
|
+
stdout = proc.stdout
|
|
9625
|
+
if stdout is None:
|
|
9626
|
+
return
|
|
9627
|
+
while True:
|
|
9628
|
+
chunk = stdout.read(4096)
|
|
9629
|
+
if not chunk:
|
|
9630
|
+
break
|
|
9631
|
+
chunks_queue.put(chunk)
|
|
9632
|
+
except Exception:
|
|
9633
|
+
pass
|
|
9634
|
+
finally:
|
|
9635
|
+
chunks_queue.put(None)
|
|
9636
|
+
|
|
9637
|
+
threading.Thread(target=_reader, daemon=True, name=f"channel-probe-stdout-{server_name}").start()
|
|
9638
|
+
|
|
9639
|
+
body = _channel_probe_initialize_payload()
|
|
9640
|
+
if framed:
|
|
9641
|
+
frame = b"Content-Length: " + str(len(body)).encode("ascii") + b"\r\n\r\n" + body
|
|
9642
|
+
else:
|
|
9643
|
+
frame = body + b"\n"
|
|
9644
|
+
try:
|
|
9645
|
+
if proc.stdin:
|
|
9646
|
+
proc.stdin.write(frame)
|
|
9647
|
+
proc.stdin.flush()
|
|
9648
|
+
except Exception:
|
|
9649
|
+
pass
|
|
9650
|
+
|
|
9651
|
+
deadline = time.time() + timeout
|
|
9652
|
+
stdout_buf = bytearray()
|
|
9653
|
+
capable = False
|
|
9654
|
+
try:
|
|
9655
|
+
while time.time() < deadline:
|
|
9656
|
+
wait = min(0.2, max(0.001, deadline - time.time()))
|
|
9657
|
+
try:
|
|
9658
|
+
chunk = chunks_queue.get(timeout=wait)
|
|
9659
|
+
except queue.Empty:
|
|
9660
|
+
continue
|
|
9661
|
+
if chunk is None:
|
|
9662
|
+
break
|
|
9663
|
+
stdout_buf.extend(chunk)
|
|
9664
|
+
response = _channel_probe_find_initialize_response(bytes(stdout_buf), framed)
|
|
9665
|
+
if response is not None:
|
|
9666
|
+
capable = _channel_probe_capability_present(response)
|
|
9667
|
+
break
|
|
9668
|
+
finally:
|
|
9669
|
+
try:
|
|
9670
|
+
if proc.stdin:
|
|
9671
|
+
proc.stdin.close()
|
|
9672
|
+
except Exception:
|
|
9673
|
+
pass
|
|
9674
|
+
try:
|
|
9675
|
+
proc.terminate()
|
|
9676
|
+
proc.wait(timeout=1.0)
|
|
9677
|
+
except Exception:
|
|
9678
|
+
try:
|
|
9679
|
+
proc.kill()
|
|
9680
|
+
except Exception:
|
|
9681
|
+
pass
|
|
9682
|
+
|
|
9683
|
+
router_log(
|
|
9684
|
+
"INFO",
|
|
9685
|
+
f"channel_probe_result server={server_name} channel_capable={capable} bytes={len(stdout_buf)}",
|
|
9686
|
+
)
|
|
9687
|
+
return capable
|
|
9688
|
+
|
|
9689
|
+
|
|
9690
|
+
def detect_channel_capable_mcp_servers(
|
|
9691
|
+
mcp_config_paths: Iterable[str],
|
|
9692
|
+
cwd: Path,
|
|
9693
|
+
*,
|
|
9694
|
+
include_router_self: bool = True,
|
|
9695
|
+
timeout_per_server: float = 3.0,
|
|
9696
|
+
) -> list[str]:
|
|
9697
|
+
"""Probe MCP servers declared in given config files; return names that declare experimental['claude/channel']."""
|
|
9698
|
+
capable: list[str] = []
|
|
9699
|
+
seen: set[str] = set()
|
|
9700
|
+
if include_router_self:
|
|
9701
|
+
capable.append("claude-any-router")
|
|
9702
|
+
seen.add("claude-any-router")
|
|
9703
|
+
for path_str in mcp_config_paths:
|
|
9704
|
+
if not path_str:
|
|
9705
|
+
continue
|
|
9706
|
+
path = Path(path_str)
|
|
9707
|
+
if not path.exists():
|
|
9708
|
+
continue
|
|
9709
|
+
for name, server in _read_mcp_servers_from_json(path, cwd):
|
|
9710
|
+
if name in seen:
|
|
9711
|
+
continue
|
|
9712
|
+
seen.add(name)
|
|
9713
|
+
if name == "claude-any-router":
|
|
9714
|
+
continue
|
|
9715
|
+
if not _mcp_server_is_stdio(server):
|
|
9716
|
+
# Non-stdio (sse/http) probing not implemented; skip silently.
|
|
9717
|
+
continue
|
|
9718
|
+
try:
|
|
9719
|
+
if probe_stdio_mcp_for_channel_capability(name, server, timeout=timeout_per_server):
|
|
9720
|
+
capable.append(name)
|
|
9721
|
+
except Exception as exc:
|
|
9722
|
+
router_log(
|
|
9723
|
+
"WARN",
|
|
9724
|
+
f"channel_probe_exception server={name} error={type(exc).__name__}: {exc}",
|
|
9725
|
+
)
|
|
9726
|
+
return capable
|
|
9727
|
+
|
|
9728
|
+
|
|
9501
9729
|
def _mcp_config_passthrough_values(passthrough: list[str]) -> list[str]:
|
|
9502
9730
|
values: list[str] = []
|
|
9503
9731
|
i = 0
|
|
@@ -13939,8 +14167,21 @@ def native_channel_passthrough_requested(passthrough: list[str]) -> bool:
|
|
|
13939
14167
|
return has_passthrough_option(passthrough, "--channels", "--dangerously-load-development-channels")
|
|
13940
14168
|
|
|
13941
14169
|
|
|
13942
|
-
def claude_channel_args(
|
|
13943
|
-
|
|
14170
|
+
def claude_channel_args(
|
|
14171
|
+
cfg: dict[str, Any],
|
|
14172
|
+
passthrough: list[str],
|
|
14173
|
+
extra_specs: list[str] | None = None,
|
|
14174
|
+
*,
|
|
14175
|
+
native_channel_bridge: bool = False,
|
|
14176
|
+
) -> list[str]:
|
|
14177
|
+
if not native_channel_bridge:
|
|
14178
|
+
return []
|
|
14179
|
+
if native_channel_passthrough_requested(passthrough):
|
|
14180
|
+
return []
|
|
14181
|
+
specs = list(channel_specs_for_launch(cfg, passthrough, extra_specs))
|
|
14182
|
+
if not specs:
|
|
14183
|
+
return []
|
|
14184
|
+
return ["--dangerously-load-development-channels", *specs]
|
|
13944
14185
|
|
|
13945
14186
|
|
|
13946
14187
|
def claude_channels_requested(cfg: dict[str, Any], passthrough: list[str], extra_specs: list[str] | None = None) -> bool:
|
|
@@ -14160,20 +14401,41 @@ def _write_fd_all(fd: int, data: bytes) -> None:
|
|
|
14160
14401
|
view = view[written:]
|
|
14161
14402
|
|
|
14162
14403
|
|
|
14404
|
+
def _channel_platform_default_enter_bytes(platform: str | None = None, os_name: str | None = None) -> bytes:
|
|
14405
|
+
sys_platform = str(platform if platform is not None else sys.platform).lower()
|
|
14406
|
+
os_family = str(os_name if os_name is not None else os.name).lower()
|
|
14407
|
+
if os_family == "nt" or sys_platform.startswith(("win", "cygwin", "msys")):
|
|
14408
|
+
return b"\r\n"
|
|
14409
|
+
if os_family == "posix":
|
|
14410
|
+
return b"\r\n"
|
|
14411
|
+
return b"\r\n"
|
|
14412
|
+
|
|
14413
|
+
|
|
14163
14414
|
def _channel_wake_enter_bytes(value: str | bytes | None = None) -> bytes:
|
|
14164
14415
|
raw: str | bytes | None = value
|
|
14165
14416
|
if raw is None:
|
|
14166
14417
|
raw = os.environ.get("CLAUDE_ANY_CHANNEL_WAKE_ENTER")
|
|
14418
|
+
if raw is None:
|
|
14419
|
+
return _channel_platform_default_enter_bytes()
|
|
14167
14420
|
if isinstance(raw, bytes):
|
|
14168
|
-
return raw if raw in (b"\n", b"\r", b"\r\n") else
|
|
14421
|
+
return raw if raw in (b"\n", b"\r", b"\r\n") else _channel_platform_default_enter_bytes()
|
|
14169
14422
|
normalized = str(raw or "").strip().lower()
|
|
14170
|
-
if normalized in {"", "
|
|
14423
|
+
if normalized in {"", "auto", "default", "platform"}:
|
|
14424
|
+
return _channel_platform_default_enter_bytes()
|
|
14425
|
+
if normalized in {"lf", "nl", "newline", "linefeed", "\\n"}:
|
|
14171
14426
|
return b"\n"
|
|
14172
14427
|
if normalized in {"cr", "return", "carriage-return", "carriage_return", "\\r"}:
|
|
14173
14428
|
return b"\r"
|
|
14174
14429
|
if normalized in {"crlf", "cr-lf", "return-newline", "\\r\\n"}:
|
|
14175
14430
|
return b"\r\n"
|
|
14176
|
-
return
|
|
14431
|
+
return _channel_platform_default_enter_bytes()
|
|
14432
|
+
|
|
14433
|
+
|
|
14434
|
+
def _channel_wake_enter_env_is_fixed() -> bool:
|
|
14435
|
+
raw = os.environ.get("CLAUDE_ANY_CHANNEL_WAKE_ENTER")
|
|
14436
|
+
if raw is None:
|
|
14437
|
+
return False
|
|
14438
|
+
return str(raw).strip().lower() not in {"", "auto", "default", "platform"}
|
|
14177
14439
|
|
|
14178
14440
|
|
|
14179
14441
|
def _channel_enter_bytes_from_user_input(data: bytes) -> bytes | None:
|
|
@@ -14269,6 +14531,10 @@ def subprocess_call_with_channel_wake_proxy(cmd: list[str], env: dict[str, str])
|
|
|
14269
14531
|
old_attrs = termios.tcgetattr(stdin_fd)
|
|
14270
14532
|
last_channel_poll = 0.0
|
|
14271
14533
|
channel_enter_bytes = _channel_wake_enter_bytes()
|
|
14534
|
+
router_log(
|
|
14535
|
+
"INFO",
|
|
14536
|
+
f"channel_stdin_proxy_enter_default enter={_channel_enter_label(channel_enter_bytes)} os={os.name} platform={sys.platform}",
|
|
14537
|
+
)
|
|
14272
14538
|
try:
|
|
14273
14539
|
tty.setraw(stdin_fd)
|
|
14274
14540
|
while proc.poll() is None:
|
|
@@ -14280,7 +14546,12 @@ def subprocess_call_with_channel_wake_proxy(cmd: list[str], env: dict[str, str])
|
|
|
14280
14546
|
data = os.read(stdin_fd, 4096)
|
|
14281
14547
|
if data:
|
|
14282
14548
|
observed_enter = _channel_synthetic_enter_bytes_from_user_input(data)
|
|
14283
|
-
if observed_enter and not
|
|
14549
|
+
if observed_enter and not _channel_wake_enter_env_is_fixed():
|
|
14550
|
+
if observed_enter != channel_enter_bytes:
|
|
14551
|
+
router_log(
|
|
14552
|
+
"INFO",
|
|
14553
|
+
f"channel_stdin_proxy_enter_observed enter={_channel_enter_label(observed_enter)}",
|
|
14554
|
+
)
|
|
14284
14555
|
channel_enter_bytes = observed_enter
|
|
14285
14556
|
_write_fd_all(master_fd, data)
|
|
14286
14557
|
if master_fd in readable:
|
|
@@ -15150,7 +15421,28 @@ def launch_claude(
|
|
|
15150
15421
|
extra_args.extend(["--mcp-config", *mcp_config_paths])
|
|
15151
15422
|
if should_append_compat_prompt(provider, cfg) and not has_passthrough_option(launch_passthrough, "--system-prompt"):
|
|
15152
15423
|
extra_args.extend(["--append-system-prompt", NON_ANTHROPIC_COMPAT_PROMPT])
|
|
15153
|
-
|
|
15424
|
+
detected_channel_specs: list[str] = []
|
|
15425
|
+
if native_channel_bridge and mcp_config_paths:
|
|
15426
|
+
try:
|
|
15427
|
+
detected_servers = detect_channel_capable_mcp_servers(
|
|
15428
|
+
mcp_config_paths,
|
|
15429
|
+
Path(os.getcwd()),
|
|
15430
|
+
)
|
|
15431
|
+
detected_channel_specs = [f"server:{name}" for name in detected_servers]
|
|
15432
|
+
router_log(
|
|
15433
|
+
"INFO",
|
|
15434
|
+
f"channel_probe_detected count={len(detected_servers)} servers={','.join(detected_servers) or '-'}",
|
|
15435
|
+
)
|
|
15436
|
+
except Exception as exc:
|
|
15437
|
+
router_log("WARN", f"channel_probe_failed error={type(exc).__name__}: {exc}")
|
|
15438
|
+
extra_args.extend(
|
|
15439
|
+
claude_channel_args(
|
|
15440
|
+
cfg,
|
|
15441
|
+
launch_passthrough,
|
|
15442
|
+
extra_specs=detected_channel_specs,
|
|
15443
|
+
native_channel_bridge=native_channel_bridge,
|
|
15444
|
+
)
|
|
15445
|
+
)
|
|
15154
15446
|
cmd = [
|
|
15155
15447
|
claude,
|
|
15156
15448
|
"--dangerously-skip-permissions",
|
package/package.json
CHANGED