@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.
Files changed (2) hide show
  1. package/claude_any.py +266 -4
  2. 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.96"
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(cfg: dict[str, Any], passthrough: list[str], extra_specs: list[str] | None = None) -> list[str]:
13943
- return []
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
- extra_args.extend(claude_channel_args(cfg, launch_passthrough))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneciel-ai/claude-any",
3
- "version": "0.1.96",
3
+ "version": "0.1.97",
4
4
  "description": "Claude Code provider selector for Anthropic, Ollama, Ollama Cloud, vLLM, NVIDIA hosted, and self-hosted NIM.",
5
5
  "license": "MIT",
6
6
  "author": "One Ciel LLC",