@oneciel-ai/claude-any 0.1.96 → 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 +266 -4
- 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:
|
|
@@ -15180,7 +15421,28 @@ def launch_claude(
|
|
|
15180
15421
|
extra_args.extend(["--mcp-config", *mcp_config_paths])
|
|
15181
15422
|
if should_append_compat_prompt(provider, cfg) and not has_passthrough_option(launch_passthrough, "--system-prompt"):
|
|
15182
15423
|
extra_args.extend(["--append-system-prompt", NON_ANTHROPIC_COMPAT_PROMPT])
|
|
15183
|
-
|
|
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
|
+
)
|
|
15184
15446
|
cmd = [
|
|
15185
15447
|
claude,
|
|
15186
15448
|
"--dangerously-skip-permissions",
|
package/package.json
CHANGED