@oneciel-ai/claude-any 0.1.97 → 0.1.98
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 +483 -23
- package/package.json +1 -1
package/claude_any.py
CHANGED
|
@@ -60,6 +60,7 @@ WEB_TOOLS_MCP_CONFIG = CONFIG_DIR / "web-tools-mcp.json"
|
|
|
60
60
|
DUCKDUCKGO_MCP_CONFIG = CONFIG_DIR / "duckduckgo-mcp.json"
|
|
61
61
|
CHANNEL_MCP_CONFIG = CONFIG_DIR / "channel-mcp.json"
|
|
62
62
|
CHANNEL_MCP_CURSOR_PATH = CONFIG_DIR / "channel-mcp-cursor.json"
|
|
63
|
+
CHANNEL_PROBE_CACHE_PATH = CONFIG_DIR / "channel-probe-cache.json"
|
|
63
64
|
MCP_PROXY_CONFIG = CONFIG_DIR / "mcp-proxy.json"
|
|
64
65
|
ROUTER_HOST = os.environ.get("CLAUDE_ANY_ROUTER_CLIENT_HOST", "127.0.0.1").strip() or "127.0.0.1"
|
|
65
66
|
ROUTER_PORT = 8799
|
|
@@ -106,7 +107,7 @@ OFFICIAL_CHANNEL_PLUGINS = {
|
|
|
106
107
|
"fakechat": "plugin:fakechat@claude-plugins-official",
|
|
107
108
|
}
|
|
108
109
|
APP_NAME = "Claude Any"
|
|
109
|
-
VERSION = "0.1.
|
|
110
|
+
VERSION = "0.1.98"
|
|
110
111
|
CREDITS = "Credits: One Ciel LLC"
|
|
111
112
|
|
|
112
113
|
LOG_LEVELS = {"SILENT": 0, "ERROR": 1, "WARN": 2, "INFO": 3, "DEBUG": 4, "TRACE": 5}
|
|
@@ -1061,6 +1062,7 @@ UI_TEXT = {
|
|
|
1061
1062
|
"test": "Test compatibility",
|
|
1062
1063
|
"options": "LLM options",
|
|
1063
1064
|
"channel_delivery": "Channel delivery",
|
|
1065
|
+
"channels": "Channels",
|
|
1064
1066
|
"log_level": "Log level",
|
|
1065
1067
|
"presets": "LLM presets",
|
|
1066
1068
|
"context_setup": "Context setup",
|
|
@@ -1083,6 +1085,7 @@ UI_TEXT = {
|
|
|
1083
1085
|
"test": "호환성 테스트",
|
|
1084
1086
|
"options": "LLM 옵션",
|
|
1085
1087
|
"channel_delivery": "채널 전달 방식",
|
|
1088
|
+
"channels": "채널",
|
|
1086
1089
|
"log_level": "로그 레벨",
|
|
1087
1090
|
"presets": "LLM 프리셋",
|
|
1088
1091
|
"context_setup": "컨텍스트 설정",
|
|
@@ -1105,6 +1108,7 @@ UI_TEXT = {
|
|
|
1105
1108
|
"test": "互換性テスト",
|
|
1106
1109
|
"options": "LLMオプション",
|
|
1107
1110
|
"channel_delivery": "チャンネル配信方式",
|
|
1111
|
+
"channels": "チャンネル",
|
|
1108
1112
|
"log_level": "ログレベル",
|
|
1109
1113
|
"presets": "LLMプリセット",
|
|
1110
1114
|
"context_setup": "コンテキスト設定",
|
|
@@ -1127,6 +1131,7 @@ UI_TEXT = {
|
|
|
1127
1131
|
"test": "兼容性测试",
|
|
1128
1132
|
"options": "LLM 选项",
|
|
1129
1133
|
"channel_delivery": "频道投递方式",
|
|
1134
|
+
"channels": "频道",
|
|
1130
1135
|
"log_level": "日志级别",
|
|
1131
1136
|
"presets": "LLM 预设",
|
|
1132
1137
|
"context_setup": "上下文设置",
|
|
@@ -9821,6 +9826,215 @@ def auto_discovered_mcp_channel_specs(
|
|
|
9821
9826
|
return _dedupe_strings(specs)
|
|
9822
9827
|
|
|
9823
9828
|
|
|
9829
|
+
CHANNEL_PROBE_CACHE_VERSION = 1
|
|
9830
|
+
|
|
9831
|
+
|
|
9832
|
+
def _builtin_router_probe_record() -> dict[str, Any]:
|
|
9833
|
+
return {
|
|
9834
|
+
"name": "claude-any-router",
|
|
9835
|
+
"capable": True,
|
|
9836
|
+
"transport": "sse",
|
|
9837
|
+
"source_path": "<built-in>",
|
|
9838
|
+
"url": f"{ROUTER_BASE}/ca/mcp/sse",
|
|
9839
|
+
"response_bytes": 0,
|
|
9840
|
+
"reason": "built-in",
|
|
9841
|
+
}
|
|
9842
|
+
|
|
9843
|
+
|
|
9844
|
+
def _server_transport_label(server: dict[str, Any]) -> str:
|
|
9845
|
+
if not isinstance(server, dict):
|
|
9846
|
+
return "unknown"
|
|
9847
|
+
declared = str(server.get("type") or "").strip().lower()
|
|
9848
|
+
if declared:
|
|
9849
|
+
return declared
|
|
9850
|
+
if server.get("url"):
|
|
9851
|
+
return "sse"
|
|
9852
|
+
if server.get("command"):
|
|
9853
|
+
return "stdio"
|
|
9854
|
+
return "unknown"
|
|
9855
|
+
|
|
9856
|
+
|
|
9857
|
+
def _probe_mcp_servers_to_records(
|
|
9858
|
+
paths: Iterable[str],
|
|
9859
|
+
cwd: Path,
|
|
9860
|
+
*,
|
|
9861
|
+
include_router_self: bool = True,
|
|
9862
|
+
timeout_per_server: float = 3.0,
|
|
9863
|
+
) -> list[dict[str, Any]]:
|
|
9864
|
+
"""Probe every MCP server referenced from the given config paths and
|
|
9865
|
+
return one record per server (capable and non-capable alike) for cache
|
|
9866
|
+
consumers and menu rendering."""
|
|
9867
|
+
records: list[dict[str, Any]] = []
|
|
9868
|
+
seen: set[str] = set()
|
|
9869
|
+
if include_router_self:
|
|
9870
|
+
records.append(_builtin_router_probe_record())
|
|
9871
|
+
seen.add("claude-any-router")
|
|
9872
|
+
for path_str in paths:
|
|
9873
|
+
if not path_str:
|
|
9874
|
+
continue
|
|
9875
|
+
path = Path(path_str)
|
|
9876
|
+
if not path.exists() or not path.is_file():
|
|
9877
|
+
continue
|
|
9878
|
+
for name, server in _read_mcp_servers_from_json(path, cwd):
|
|
9879
|
+
if name in seen:
|
|
9880
|
+
continue
|
|
9881
|
+
seen.add(name)
|
|
9882
|
+
if name == "claude-any-router":
|
|
9883
|
+
continue
|
|
9884
|
+
transport = _server_transport_label(server)
|
|
9885
|
+
record: dict[str, Any] = {
|
|
9886
|
+
"name": name,
|
|
9887
|
+
"capable": False,
|
|
9888
|
+
"transport": transport,
|
|
9889
|
+
"source_path": str(path),
|
|
9890
|
+
"response_bytes": 0,
|
|
9891
|
+
"reason": "",
|
|
9892
|
+
}
|
|
9893
|
+
if isinstance(server.get("url"), str):
|
|
9894
|
+
record["url"] = str(server.get("url"))
|
|
9895
|
+
if not _mcp_server_is_stdio(server):
|
|
9896
|
+
record["reason"] = "non_stdio_probe_not_implemented"
|
|
9897
|
+
records.append(record)
|
|
9898
|
+
continue
|
|
9899
|
+
try:
|
|
9900
|
+
capable = probe_stdio_mcp_for_channel_capability(name, server, timeout=timeout_per_server)
|
|
9901
|
+
record["capable"] = bool(capable)
|
|
9902
|
+
if not capable:
|
|
9903
|
+
record["reason"] = "no_experimental_claude_channel_or_timeout"
|
|
9904
|
+
except Exception as exc:
|
|
9905
|
+
record["reason"] = f"probe_exception:{type(exc).__name__}"
|
|
9906
|
+
router_log("WARN", f"channel_probe_exception server={name} error={type(exc).__name__}: {exc}")
|
|
9907
|
+
records.append(record)
|
|
9908
|
+
return records
|
|
9909
|
+
|
|
9910
|
+
|
|
9911
|
+
def read_channel_probe_cache() -> dict[str, Any]:
|
|
9912
|
+
if not CHANNEL_PROBE_CACHE_PATH.exists():
|
|
9913
|
+
return {"version": CHANNEL_PROBE_CACHE_VERSION, "probed_at": 0.0, "servers": []}
|
|
9914
|
+
try:
|
|
9915
|
+
data = json.loads(CHANNEL_PROBE_CACHE_PATH.read_text(encoding="utf-8"))
|
|
9916
|
+
except Exception as exc:
|
|
9917
|
+
router_log("WARN", f"channel_probe_cache_read_failed error={type(exc).__name__}: {exc}")
|
|
9918
|
+
return {"version": CHANNEL_PROBE_CACHE_VERSION, "probed_at": 0.0, "servers": []}
|
|
9919
|
+
if not isinstance(data, dict):
|
|
9920
|
+
return {"version": CHANNEL_PROBE_CACHE_VERSION, "probed_at": 0.0, "servers": []}
|
|
9921
|
+
data.setdefault("version", CHANNEL_PROBE_CACHE_VERSION)
|
|
9922
|
+
data.setdefault("probed_at", 0.0)
|
|
9923
|
+
servers = data.get("servers")
|
|
9924
|
+
data["servers"] = [item for item in servers if isinstance(item, dict)] if isinstance(servers, list) else []
|
|
9925
|
+
return data
|
|
9926
|
+
|
|
9927
|
+
|
|
9928
|
+
def _write_channel_probe_cache(cache: dict[str, Any]) -> None:
|
|
9929
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
9930
|
+
tmp = CHANNEL_PROBE_CACHE_PATH.with_suffix(".json.tmp")
|
|
9931
|
+
tmp.write_text(json.dumps(cache, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
9932
|
+
tmp.replace(CHANNEL_PROBE_CACHE_PATH)
|
|
9933
|
+
try:
|
|
9934
|
+
os.chmod(CHANNEL_PROBE_CACHE_PATH, 0o600)
|
|
9935
|
+
except Exception:
|
|
9936
|
+
pass
|
|
9937
|
+
|
|
9938
|
+
|
|
9939
|
+
def refresh_channel_probe_cache(
|
|
9940
|
+
passthrough: list[str] | None = None,
|
|
9941
|
+
cwd: Path | None = None,
|
|
9942
|
+
home: Path | None = None,
|
|
9943
|
+
timeout_per_server: float = 3.0,
|
|
9944
|
+
) -> dict[str, Any]:
|
|
9945
|
+
"""Re-scan known MCP config files, probe each stdio entry for the
|
|
9946
|
+
channel capability, write the result to disk and return it. Only called
|
|
9947
|
+
on explicit user action (menu refresh or CLI subcommand)."""
|
|
9948
|
+
cwd = cwd or Path.cwd()
|
|
9949
|
+
paths = [str(p) for p in claude_mcp_config_paths(passthrough or [], cwd, home)]
|
|
9950
|
+
records = _probe_mcp_servers_to_records(paths, cwd, timeout_per_server=timeout_per_server)
|
|
9951
|
+
cache = {
|
|
9952
|
+
"version": CHANNEL_PROBE_CACHE_VERSION,
|
|
9953
|
+
"probed_at": time.time(),
|
|
9954
|
+
"servers": records,
|
|
9955
|
+
}
|
|
9956
|
+
_write_channel_probe_cache(cache)
|
|
9957
|
+
capable = [r["name"] for r in records if r.get("capable")]
|
|
9958
|
+
router_log(
|
|
9959
|
+
"INFO",
|
|
9960
|
+
f"channel_probe_cache_refreshed total={len(records)} capable={len(capable)} servers={','.join(capable) or '-'}",
|
|
9961
|
+
)
|
|
9962
|
+
return cache
|
|
9963
|
+
|
|
9964
|
+
|
|
9965
|
+
def cached_channel_probe_servers() -> list[dict[str, Any]]:
|
|
9966
|
+
cache = read_channel_probe_cache()
|
|
9967
|
+
servers = cache.get("servers")
|
|
9968
|
+
if isinstance(servers, list):
|
|
9969
|
+
return [item for item in servers if isinstance(item, dict)]
|
|
9970
|
+
return []
|
|
9971
|
+
|
|
9972
|
+
|
|
9973
|
+
def cached_channel_capable_server_names() -> list[str]:
|
|
9974
|
+
"""Capable server names from cache, with claude-any-router always
|
|
9975
|
+
present (because the router's own MCP bridge is built into claude-any)."""
|
|
9976
|
+
names = [str(r.get("name")) for r in cached_channel_probe_servers() if r.get("capable") and r.get("name")]
|
|
9977
|
+
if "claude-any-router" not in names:
|
|
9978
|
+
names.insert(0, "claude-any-router")
|
|
9979
|
+
return _dedupe_strings(names)
|
|
9980
|
+
|
|
9981
|
+
|
|
9982
|
+
def parse_passthrough_channel_specs(passthrough: list[str]) -> list[str]:
|
|
9983
|
+
"""Extract channel specs from passthrough args of either
|
|
9984
|
+
--channels or --dangerously-load-development-channels (and the
|
|
9985
|
+
=VALUE inline form)."""
|
|
9986
|
+
specs: list[str] = []
|
|
9987
|
+
i = 0
|
|
9988
|
+
while i < len(passthrough):
|
|
9989
|
+
arg = passthrough[i]
|
|
9990
|
+
if arg in ("--channels", "--dangerously-load-development-channels"):
|
|
9991
|
+
i += 1
|
|
9992
|
+
while i < len(passthrough) and is_channel_spec_tagged(passthrough[i]):
|
|
9993
|
+
specs.append(passthrough[i])
|
|
9994
|
+
i += 1
|
|
9995
|
+
continue
|
|
9996
|
+
if arg.startswith("--channels=") or arg.startswith("--dangerously-load-development-channels="):
|
|
9997
|
+
value = arg.split("=", 1)[1].strip()
|
|
9998
|
+
if value and is_channel_spec_tagged(value):
|
|
9999
|
+
specs.append(value)
|
|
10000
|
+
i += 1
|
|
10001
|
+
continue
|
|
10002
|
+
i += 1
|
|
10003
|
+
return _dedupe_strings(specs)
|
|
10004
|
+
|
|
10005
|
+
|
|
10006
|
+
def auto_import_passthrough_channels(passthrough: list[str]) -> list[str]:
|
|
10007
|
+
"""Add channel specs that arrived as CLI passthrough to the persisted
|
|
10008
|
+
cfg.channels list, so they show up alongside auto-detected entries in
|
|
10009
|
+
the menu and survive subsequent launches. Returns the newly added specs."""
|
|
10010
|
+
specs = parse_passthrough_channel_specs(passthrough)
|
|
10011
|
+
if not specs:
|
|
10012
|
+
return []
|
|
10013
|
+
cfg = load_config()
|
|
10014
|
+
existing = set(channel_specs(cfg))
|
|
10015
|
+
if all(spec in existing for spec in specs):
|
|
10016
|
+
return []
|
|
10017
|
+
cc = cfg.setdefault("claude_code", {})
|
|
10018
|
+
merged = list(channel_specs(cfg))
|
|
10019
|
+
added: list[str] = []
|
|
10020
|
+
for spec in specs:
|
|
10021
|
+
if spec in existing:
|
|
10022
|
+
continue
|
|
10023
|
+
merged.append(spec)
|
|
10024
|
+
existing.add(spec)
|
|
10025
|
+
added.append(spec)
|
|
10026
|
+
if not added:
|
|
10027
|
+
return []
|
|
10028
|
+
cc["channels"] = merged
|
|
10029
|
+
save_config(cfg)
|
|
10030
|
+
invalidate_config_cache()
|
|
10031
|
+
router_log(
|
|
10032
|
+
"INFO",
|
|
10033
|
+
f"channels_auto_imported_from_passthrough count={len(added)} specs={','.join(added)}",
|
|
10034
|
+
)
|
|
10035
|
+
return added
|
|
10036
|
+
|
|
10037
|
+
|
|
9824
10038
|
def _mcp_sse_servers_from_mapping(mapping: Any) -> list[dict[str, Any]]:
|
|
9825
10039
|
if not isinstance(mapping, dict):
|
|
9826
10040
|
return []
|
|
@@ -10023,6 +10237,29 @@ def cmd_channels(args: argparse.Namespace) -> None:
|
|
|
10023
10237
|
for line in clear_channel_specs():
|
|
10024
10238
|
print(line)
|
|
10025
10239
|
return
|
|
10240
|
+
if head in ("detect", "probe", "refresh"):
|
|
10241
|
+
try:
|
|
10242
|
+
result = refresh_channel_probe_cache()
|
|
10243
|
+
except Exception as exc:
|
|
10244
|
+
raise SystemExit(f"Channel probe failed: {type(exc).__name__}: {exc}")
|
|
10245
|
+
servers = result.get("servers") or []
|
|
10246
|
+
capable = [r for r in servers if r.get("capable")]
|
|
10247
|
+
non_capable = [r for r in servers if not r.get("capable")]
|
|
10248
|
+
probed_at = result.get("probed_at") or 0
|
|
10249
|
+
ts_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(probed_at)) if probed_at else "-"
|
|
10250
|
+
print(f"channel probe complete (cached at {ts_str})")
|
|
10251
|
+
print(f" capable : {len(capable)}")
|
|
10252
|
+
for r in capable:
|
|
10253
|
+
transport = r.get("transport") or "?"
|
|
10254
|
+
source = r.get("source_path") or ""
|
|
10255
|
+
suffix = " built-in" if source == "<built-in>" else f" {source}"
|
|
10256
|
+
print(f" * {r.get('name')} ({transport}){suffix}")
|
|
10257
|
+
print(f" non-capable : {len(non_capable)}")
|
|
10258
|
+
for r in non_capable:
|
|
10259
|
+
transport = r.get("transport") or "?"
|
|
10260
|
+
reason = r.get("reason") or "-"
|
|
10261
|
+
print(f" {r.get('name')} ({transport}) reason={reason}")
|
|
10262
|
+
return
|
|
10026
10263
|
if head in ("delivery", "mode"):
|
|
10027
10264
|
if len(values) < 2:
|
|
10028
10265
|
print(f"channel_delivery: {channel_delivery_mode(cfg)}")
|
|
@@ -13239,9 +13476,10 @@ def main_menu_rows(cfg: dict[str, Any], provider: str, pcfg: dict[str, Any], lan
|
|
|
13239
13476
|
f"5. {ui_text('advisor_model', lang)} [{compact_text(pcfg.get('advisor_model') or 'off', 62)}]",
|
|
13240
13477
|
f"6. {ui_text('options', lang)} [{compact_text(llm_options_status(provider, pcfg), 62)}]",
|
|
13241
13478
|
f"7. {ui_text('channel_delivery', lang)} [{channel_delivery_mode(cfg)}]",
|
|
13242
|
-
f"8. {ui_text('
|
|
13243
|
-
f"9. {ui_text('
|
|
13244
|
-
f"10. {ui_text('
|
|
13479
|
+
f"8. {ui_text('channels', lang)} [{channel_status_text(cfg)}]",
|
|
13480
|
+
f"9. {ui_text('log_level', lang)} [{log_level_status()}]",
|
|
13481
|
+
f"10. {ui_text('test', lang)}",
|
|
13482
|
+
f"11. {ui_text('launch', lang)}",
|
|
13245
13483
|
ui_text("quit", lang),
|
|
13246
13484
|
]
|
|
13247
13485
|
|
|
@@ -13338,13 +13576,71 @@ def advisor_model_panel_rows(provider: str, pcfg: dict[str, Any]) -> tuple[list[
|
|
|
13338
13576
|
|
|
13339
13577
|
|
|
13340
13578
|
def channel_panel_rows(cfg: dict[str, Any]) -> tuple[list[str], list[str]]:
|
|
13341
|
-
channels = channel_specs(cfg)
|
|
13579
|
+
channels = set(channel_specs(cfg))
|
|
13580
|
+
cache = read_channel_probe_cache()
|
|
13581
|
+
records = cache.get("servers") or []
|
|
13582
|
+
probed_at = cache.get("probed_at") or 0
|
|
13583
|
+
capable_records = [r for r in records if r.get("capable")]
|
|
13584
|
+
non_capable_records = [r for r in records if not r.get("capable")]
|
|
13585
|
+
|
|
13342
13586
|
rows: list[str] = []
|
|
13343
13587
|
values: list[str] = []
|
|
13344
|
-
|
|
13588
|
+
|
|
13589
|
+
rows.append("[Auto-detected channel-capable]")
|
|
13590
|
+
values.append("__heading__")
|
|
13591
|
+
if capable_records:
|
|
13592
|
+
for r in capable_records:
|
|
13593
|
+
name = str(r.get("name") or "")
|
|
13594
|
+
spec = f"server:{name}"
|
|
13595
|
+
mark = "*" if spec in channels else " "
|
|
13596
|
+
transport = str(r.get("transport") or "?")
|
|
13597
|
+
source = str(r.get("source_path") or "")
|
|
13598
|
+
if source == "<built-in>":
|
|
13599
|
+
rows.append(f"{mark} {name:<14} ({transport}, built-in)")
|
|
13600
|
+
else:
|
|
13601
|
+
rows.append(f"{mark} {name:<14} ({transport})")
|
|
13602
|
+
values.append(spec)
|
|
13603
|
+
else:
|
|
13604
|
+
hint = "press Re-probe now" if not probed_at else "none capable"
|
|
13605
|
+
rows.append(f" ({hint})")
|
|
13606
|
+
values.append("__noop__")
|
|
13607
|
+
|
|
13608
|
+
if non_capable_records:
|
|
13609
|
+
rows.append("[Detected but not channel-capable]")
|
|
13610
|
+
values.append("__heading__")
|
|
13611
|
+
for r in non_capable_records:
|
|
13612
|
+
name = str(r.get("name") or "")
|
|
13613
|
+
transport = str(r.get("transport") or "?")
|
|
13614
|
+
reason = str(r.get("reason") or "-")
|
|
13615
|
+
rows.append(f" {name:<14} ({transport}) {reason}")
|
|
13616
|
+
values.append("__noop__")
|
|
13617
|
+
|
|
13618
|
+
rows.append("[Official plugins]")
|
|
13619
|
+
values.append("__heading__")
|
|
13620
|
+
for plugin_name, spec in OFFICIAL_CHANNEL_PLUGINS.items():
|
|
13345
13621
|
mark = "*" if spec in channels else " "
|
|
13346
|
-
rows.append(f"{mark} {
|
|
13622
|
+
rows.append(f"{mark} {plugin_name:<10} {spec}")
|
|
13347
13623
|
values.append(spec)
|
|
13624
|
+
|
|
13625
|
+
covered: set[str] = set(OFFICIAL_CHANNEL_PLUGINS.values())
|
|
13626
|
+
for r in capable_records:
|
|
13627
|
+
covered.add(f"server:{r.get('name')}")
|
|
13628
|
+
custom_specs = [spec for spec in channel_specs(cfg) if spec not in covered]
|
|
13629
|
+
if custom_specs:
|
|
13630
|
+
rows.append("[Configured custom / imported]")
|
|
13631
|
+
values.append("__heading__")
|
|
13632
|
+
for spec in custom_specs:
|
|
13633
|
+
rows.append(f"* {spec}")
|
|
13634
|
+
values.append(spec)
|
|
13635
|
+
|
|
13636
|
+
rows.append("[Actions]")
|
|
13637
|
+
values.append("__heading__")
|
|
13638
|
+
if probed_at:
|
|
13639
|
+
ts_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(probed_at))
|
|
13640
|
+
rows.append(f"Re-probe now (last: {ts_str})")
|
|
13641
|
+
else:
|
|
13642
|
+
rows.append("Re-probe now (no cache yet)")
|
|
13643
|
+
values.append("__reprobe__")
|
|
13348
13644
|
rows.append("+ Add custom channel...")
|
|
13349
13645
|
values.append("__add_custom__")
|
|
13350
13646
|
if channels:
|
|
@@ -13357,6 +13653,25 @@ def channel_panel_rows(cfg: dict[str, Any]) -> tuple[list[str], list[str]]:
|
|
|
13357
13653
|
return rows, values
|
|
13358
13654
|
|
|
13359
13655
|
|
|
13656
|
+
def _channel_panel_first_selectable(values: list[str]) -> int:
|
|
13657
|
+
for idx, value in enumerate(values):
|
|
13658
|
+
if value not in ("__heading__", "__noop__"):
|
|
13659
|
+
return idx
|
|
13660
|
+
return 0
|
|
13661
|
+
|
|
13662
|
+
|
|
13663
|
+
def _channel_panel_step(values: list[str], start: int, delta: int) -> int:
|
|
13664
|
+
if not values:
|
|
13665
|
+
return 0
|
|
13666
|
+
n = len(values)
|
|
13667
|
+
idx = start
|
|
13668
|
+
for _ in range(n):
|
|
13669
|
+
idx = (idx + delta) % n
|
|
13670
|
+
if values[idx] not in ("__heading__", "__noop__"):
|
|
13671
|
+
return idx
|
|
13672
|
+
return start
|
|
13673
|
+
|
|
13674
|
+
|
|
13360
13675
|
def channel_delivery_panel_rows(cfg: dict[str, Any]) -> tuple[list[str], list[str]]:
|
|
13361
13676
|
current = channel_delivery_mode(cfg)
|
|
13362
13677
|
rows = [
|
|
@@ -13668,9 +13983,10 @@ def portable_language_menu() -> int:
|
|
|
13668
13983
|
return 0
|
|
13669
13984
|
|
|
13670
13985
|
|
|
13671
|
-
def portable_prelaunch_menu() -> int:
|
|
13986
|
+
def portable_prelaunch_menu(passthrough: list[str] | None = None) -> int:
|
|
13987
|
+
passthrough = list(passthrough or [])
|
|
13672
13988
|
enable_ansi()
|
|
13673
|
-
main_idx =
|
|
13989
|
+
main_idx = 11 if settings_ready_except_api_key() else 0
|
|
13674
13990
|
panel: str | None = None
|
|
13675
13991
|
panel_idx = 0
|
|
13676
13992
|
panel_rows: list[str] = []
|
|
@@ -13718,6 +14034,8 @@ def portable_prelaunch_menu() -> int:
|
|
|
13718
14034
|
panel_rows, panel_values = log_level_panel_rows(cfg)
|
|
13719
14035
|
elif name == "channels":
|
|
13720
14036
|
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
14037
|
+
if panel_values:
|
|
14038
|
+
panel_idx = _channel_panel_first_selectable(panel_values)
|
|
13721
14039
|
elif name == "context":
|
|
13722
14040
|
panel_rows, panel_values = context_setup_panel_rows(provider, pcfg, cfg.get("language", "en"))
|
|
13723
14041
|
elif name == "preset":
|
|
@@ -13785,11 +14103,17 @@ def portable_prelaunch_menu() -> int:
|
|
|
13785
14103
|
if panel:
|
|
13786
14104
|
panel_name = panel
|
|
13787
14105
|
if key in ("up", "k"):
|
|
13788
|
-
|
|
14106
|
+
if panel == "channels":
|
|
14107
|
+
panel_idx = _channel_panel_step(panel_values, panel_idx, -1)
|
|
14108
|
+
else:
|
|
14109
|
+
panel_idx = (panel_idx - 1) % max(1, len(panel_rows))
|
|
13789
14110
|
panel_last_idx[panel_name] = panel_idx
|
|
13790
14111
|
continue
|
|
13791
14112
|
if key in ("down", "j"):
|
|
13792
|
-
|
|
14113
|
+
if panel == "channels":
|
|
14114
|
+
panel_idx = _channel_panel_step(panel_values, panel_idx, 1)
|
|
14115
|
+
else:
|
|
14116
|
+
panel_idx = (panel_idx + 1) % max(1, len(panel_rows))
|
|
13793
14117
|
panel_last_idx[panel_name] = panel_idx
|
|
13794
14118
|
continue
|
|
13795
14119
|
if key in ("esc", "left", "q"):
|
|
@@ -13894,7 +14218,7 @@ def portable_prelaunch_menu() -> int:
|
|
|
13894
14218
|
messages = lines[-8:] if lines else ["Test produced no output."]
|
|
13895
14219
|
panel_rows, panel_values = ["Run compatibility test again", "Back"], ["run", "back"]
|
|
13896
14220
|
refresh_checks()
|
|
13897
|
-
main_idx =
|
|
14221
|
+
main_idx = 11 if "Compatibility: OK" in out else 4
|
|
13898
14222
|
elif panel == "log-level":
|
|
13899
14223
|
if value == "back":
|
|
13900
14224
|
close_panel()
|
|
@@ -13916,23 +14240,43 @@ def portable_prelaunch_menu() -> int:
|
|
|
13916
14240
|
elif panel == "channels":
|
|
13917
14241
|
if value == "back":
|
|
13918
14242
|
close_panel()
|
|
14243
|
+
elif value in ("__heading__", "__noop__"):
|
|
14244
|
+
continue
|
|
14245
|
+
elif value == "__reprobe__":
|
|
14246
|
+
panel_rows, panel_values = ["Re-probing MCP channel capability..."], []
|
|
14247
|
+
first_render = render_prelaunch_screen(main_idx, panel, 0, panel_rows, checks, messages, first_render)
|
|
14248
|
+
try:
|
|
14249
|
+
result = refresh_channel_probe_cache(passthrough)
|
|
14250
|
+
capable = [r for r in result.get("servers") or [] if r.get("capable")]
|
|
14251
|
+
messages = [f"Probe complete: {len(capable)} channel-capable server(s)."]
|
|
14252
|
+
except Exception as exc:
|
|
14253
|
+
messages = [f"Re-probe failed: {type(exc).__name__}: {exc}"]
|
|
14254
|
+
cfg = load_config()
|
|
14255
|
+
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
14256
|
+
if panel_values:
|
|
14257
|
+
panel_idx = _channel_panel_first_selectable(panel_values)
|
|
13919
14258
|
elif value == "__add_custom__":
|
|
13920
14259
|
spec = prompt_menu_value("Channel spec (for example plugin:ainet@local or server:ainet)", restore_tty=restore_line_mode, raw_tty=restore_raw_mode)
|
|
13921
14260
|
if spec:
|
|
13922
14261
|
messages = add_channel_spec(spec)
|
|
13923
14262
|
cfg = load_config()
|
|
13924
14263
|
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
14264
|
+
if panel_values:
|
|
14265
|
+
panel_idx = _channel_panel_first_selectable(panel_values)
|
|
13925
14266
|
elif value == "__remove__":
|
|
13926
14267
|
spec = prompt_menu_value("Channel spec to remove", "", restore_tty=restore_line_mode, raw_tty=restore_raw_mode)
|
|
13927
14268
|
if spec:
|
|
13928
14269
|
messages = remove_channel_spec(spec)
|
|
13929
14270
|
cfg = load_config()
|
|
13930
14271
|
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
14272
|
+
if panel_values:
|
|
14273
|
+
panel_idx = _channel_panel_first_selectable(panel_values)
|
|
13931
14274
|
elif value == "__clear__":
|
|
13932
14275
|
messages = clear_channel_specs()
|
|
13933
14276
|
cfg = load_config()
|
|
13934
14277
|
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
13935
|
-
|
|
14278
|
+
if panel_values:
|
|
14279
|
+
panel_idx = _channel_panel_first_selectable(panel_values)
|
|
13936
14280
|
elif value:
|
|
13937
14281
|
if value in channel_specs(cfg):
|
|
13938
14282
|
messages = remove_channel_spec(value)
|
|
@@ -14049,7 +14393,7 @@ def portable_prelaunch_menu() -> int:
|
|
|
14049
14393
|
elif key in ("esc", "q"):
|
|
14050
14394
|
return 10
|
|
14051
14395
|
elif key == "enter":
|
|
14052
|
-
actions = ["language", "provider", "api-key", "base-url", "model", "advisor-model", "options", "channel-delivery", "log-level", "test", "launch", "quit"]
|
|
14396
|
+
actions = ["language", "provider", "api-key", "base-url", "model", "advisor-model", "options", "channel-delivery", "channels", "log-level", "test", "launch", "quit"]
|
|
14053
14397
|
action = actions[main_idx]
|
|
14054
14398
|
if action == "launch":
|
|
14055
14399
|
blockers = launch_readiness_errors()
|
|
@@ -14096,14 +14440,16 @@ def run_prelaunch_menu(passthrough: list[str], skip_menu: bool = False, force_me
|
|
|
14096
14440
|
rc = run_external_menu("claude-any-menu")
|
|
14097
14441
|
if rc is not None:
|
|
14098
14442
|
return rc
|
|
14099
|
-
return portable_prelaunch_menu()
|
|
14443
|
+
return portable_prelaunch_menu(passthrough)
|
|
14100
14444
|
|
|
14101
14445
|
|
|
14102
14446
|
def start_router_if_needed() -> None:
|
|
14103
14447
|
if router_up():
|
|
14448
|
+
router_log("INFO", f"router_check_state running=True spawn=False base={ROUTER_BASE}")
|
|
14104
14449
|
return
|
|
14105
14450
|
stop_router_processes(quiet=True)
|
|
14106
14451
|
if router_up():
|
|
14452
|
+
router_log("INFO", f"router_check_state running=True spawn=False after_stop=True base={ROUTER_BASE}")
|
|
14107
14453
|
return
|
|
14108
14454
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
14109
14455
|
cmd = [sys.executable, str(Path(__file__).resolve()), "serve"]
|
|
@@ -14114,11 +14460,13 @@ def start_router_if_needed() -> None:
|
|
|
14114
14460
|
kwargs["creationflags"] = flags
|
|
14115
14461
|
else:
|
|
14116
14462
|
kwargs["start_new_session"] = True
|
|
14463
|
+
router_log("INFO", f"router_check_state running=False spawn=True base={ROUTER_BASE}")
|
|
14117
14464
|
with open(LOG_PATH, "ab", buffering=0) as log:
|
|
14118
14465
|
subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=log, stderr=log, **kwargs)
|
|
14119
14466
|
deadline = time.time() + 30
|
|
14120
14467
|
while time.time() < deadline:
|
|
14121
14468
|
if router_up():
|
|
14469
|
+
router_log("INFO", f"router_spawned running=True base={ROUTER_BASE} elapsed={time.time()-(deadline-30):.1f}s")
|
|
14122
14470
|
return
|
|
14123
14471
|
time.sleep(0.5)
|
|
14124
14472
|
raise RuntimeError(f"claude-any router did not start. See {LOG_PATH}")
|
|
@@ -15344,6 +15692,7 @@ def launch_claude(
|
|
|
15344
15692
|
update_check = False
|
|
15345
15693
|
self_update_check = False
|
|
15346
15694
|
run_claude_any_update_check(enabled=self_update_check)
|
|
15695
|
+
auto_import_passthrough_channels(passthrough)
|
|
15347
15696
|
rc = run_prelaunch_menu(passthrough, skip_menu=skip_menu, force_menu=force_menu)
|
|
15348
15697
|
if rc == 10:
|
|
15349
15698
|
return 0
|
|
@@ -15422,19 +15771,17 @@ def launch_claude(
|
|
|
15422
15771
|
if should_append_compat_prompt(provider, cfg) and not has_passthrough_option(launch_passthrough, "--system-prompt"):
|
|
15423
15772
|
extra_args.extend(["--append-system-prompt", NON_ANTHROPIC_COMPAT_PROMPT])
|
|
15424
15773
|
detected_channel_specs: list[str] = []
|
|
15425
|
-
if native_channel_bridge
|
|
15774
|
+
if native_channel_bridge:
|
|
15426
15775
|
try:
|
|
15427
|
-
|
|
15428
|
-
|
|
15429
|
-
|
|
15430
|
-
)
|
|
15431
|
-
detected_channel_specs = [f"server:{name}" for name in detected_servers]
|
|
15776
|
+
capable_names = cached_channel_capable_server_names()
|
|
15777
|
+
detected_channel_specs = [f"server:{name}" for name in capable_names]
|
|
15778
|
+
cache_age = read_channel_probe_cache().get("probed_at") or 0
|
|
15432
15779
|
router_log(
|
|
15433
15780
|
"INFO",
|
|
15434
|
-
f"
|
|
15781
|
+
f"channel_probe_loaded source=cache cache_age_ts={int(cache_age)} count={len(capable_names)} servers={','.join(capable_names) or '-'}",
|
|
15435
15782
|
)
|
|
15436
15783
|
except Exception as exc:
|
|
15437
|
-
router_log("WARN", f"
|
|
15784
|
+
router_log("WARN", f"channel_probe_cache_load_failed error={type(exc).__name__}: {exc}")
|
|
15438
15785
|
extra_args.extend(
|
|
15439
15786
|
claude_channel_args(
|
|
15440
15787
|
cfg,
|
|
@@ -15452,11 +15799,124 @@ def launch_claude(
|
|
|
15452
15799
|
cmd.extend(["--model", model])
|
|
15453
15800
|
cmd.extend(extra_args)
|
|
15454
15801
|
cmd.extend(claude_passthrough)
|
|
15802
|
+
_log_claude_command_for_diagnostics(cmd, env)
|
|
15803
|
+
capture_stderr = env_bool(os.environ.get("CLAUDE_ANY_CAPTURE_CC_STDERR"), False)
|
|
15455
15804
|
if stdin_channel_proxy:
|
|
15456
15805
|
return subprocess_call_with_channel_wake_proxy(cmd, env)
|
|
15806
|
+
if capture_stderr:
|
|
15807
|
+
return _subprocess_call_capturing_stderr(cmd, env)
|
|
15457
15808
|
return subprocess.call(cmd, env=env)
|
|
15458
15809
|
|
|
15459
15810
|
|
|
15811
|
+
CLAUDE_CODE_STDERR_LOG = CONFIG_DIR / "claude-code-stderr.log"
|
|
15812
|
+
|
|
15813
|
+
|
|
15814
|
+
def _log_claude_command_for_diagnostics(cmd: list[str], env: dict[str, str]) -> None:
|
|
15815
|
+
try:
|
|
15816
|
+
mcp_idx = cmd.index("--mcp-config") if "--mcp-config" in cmd else -1
|
|
15817
|
+
mcp_value = cmd[mcp_idx + 1] if 0 <= mcp_idx < len(cmd) - 1 else "-"
|
|
15818
|
+
except Exception:
|
|
15819
|
+
mcp_value = "-"
|
|
15820
|
+
channel_specs_in_cmd: list[str] = []
|
|
15821
|
+
if "--dangerously-load-development-channels" in cmd:
|
|
15822
|
+
start = cmd.index("--dangerously-load-development-channels") + 1
|
|
15823
|
+
for arg in cmd[start:]:
|
|
15824
|
+
if arg.startswith("--"):
|
|
15825
|
+
break
|
|
15826
|
+
channel_specs_in_cmd.append(arg)
|
|
15827
|
+
router_log(
|
|
15828
|
+
"INFO",
|
|
15829
|
+
"claude_launch_cmd mcp_config=%s channels=%s argv_len=%d"
|
|
15830
|
+
% (
|
|
15831
|
+
mcp_value,
|
|
15832
|
+
",".join(channel_specs_in_cmd) or "-",
|
|
15833
|
+
len(cmd),
|
|
15834
|
+
),
|
|
15835
|
+
)
|
|
15836
|
+
relevant_env_keys = (
|
|
15837
|
+
"ANTHROPIC_BASE_URL",
|
|
15838
|
+
"ANTHROPIC_MODEL",
|
|
15839
|
+
"ANTHROPIC_API_KEY",
|
|
15840
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
15841
|
+
"CLAUDE_ANY_PROVIDER",
|
|
15842
|
+
"CLAUDE_ANY_MODEL_ALIAS",
|
|
15843
|
+
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS",
|
|
15844
|
+
)
|
|
15845
|
+
env_summary = []
|
|
15846
|
+
for key in relevant_env_keys:
|
|
15847
|
+
if key in env:
|
|
15848
|
+
val = env[key]
|
|
15849
|
+
if "KEY" in key or "TOKEN" in key:
|
|
15850
|
+
val = mask_secret(val)
|
|
15851
|
+
env_summary.append(f"{key}={val}")
|
|
15852
|
+
if env_summary:
|
|
15853
|
+
router_log("INFO", "claude_launch_env " + " ".join(env_summary))
|
|
15854
|
+
|
|
15855
|
+
|
|
15856
|
+
def _subprocess_call_capturing_stderr(cmd: list[str], env: dict[str, str]) -> int:
|
|
15857
|
+
"""Like subprocess.call but tees Claude Code's stderr into
|
|
15858
|
+
~/.config/claude-any/claude-code-stderr.log so the user can collect
|
|
15859
|
+
the exact context around messages like
|
|
15860
|
+
`--dangerously-load-development-channels ignored (server:...)`.
|
|
15861
|
+
|
|
15862
|
+
Enabled via CLAUDE_ANY_CAPTURE_CC_STDERR=1."""
|
|
15863
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
15864
|
+
try:
|
|
15865
|
+
if CLAUDE_CODE_STDERR_LOG.exists() and CLAUDE_CODE_STDERR_LOG.stat().st_size > 2_000_000:
|
|
15866
|
+
CLAUDE_CODE_STDERR_LOG.replace(CLAUDE_CODE_STDERR_LOG.with_suffix(".log.1"))
|
|
15867
|
+
except Exception:
|
|
15868
|
+
pass
|
|
15869
|
+
try:
|
|
15870
|
+
log_handle = CLAUDE_CODE_STDERR_LOG.open("ab", buffering=0)
|
|
15871
|
+
except Exception as exc:
|
|
15872
|
+
router_log("WARN", f"claude_stderr_capture_open_failed error={type(exc).__name__}: {exc}")
|
|
15873
|
+
return subprocess.call(cmd, env=env)
|
|
15874
|
+
header = f"\n===== claude launch at {time.strftime('%Y-%m-%dT%H:%M:%S')} =====\n".encode("utf-8")
|
|
15875
|
+
try:
|
|
15876
|
+
log_handle.write(header)
|
|
15877
|
+
except Exception:
|
|
15878
|
+
pass
|
|
15879
|
+
try:
|
|
15880
|
+
proc = subprocess.Popen(cmd, env=env, stderr=subprocess.PIPE)
|
|
15881
|
+
except Exception as exc:
|
|
15882
|
+
router_log("WARN", f"claude_stderr_capture_spawn_failed error={type(exc).__name__}: {exc}")
|
|
15883
|
+
try:
|
|
15884
|
+
log_handle.close()
|
|
15885
|
+
except Exception:
|
|
15886
|
+
pass
|
|
15887
|
+
return subprocess.call(cmd, env=env)
|
|
15888
|
+
|
|
15889
|
+
def _tee_stderr() -> None:
|
|
15890
|
+
try:
|
|
15891
|
+
if proc.stderr is None:
|
|
15892
|
+
return
|
|
15893
|
+
while True:
|
|
15894
|
+
chunk = proc.stderr.read(4096)
|
|
15895
|
+
if not chunk:
|
|
15896
|
+
break
|
|
15897
|
+
try:
|
|
15898
|
+
sys.stderr.buffer.write(chunk)
|
|
15899
|
+
sys.stderr.buffer.flush()
|
|
15900
|
+
except Exception:
|
|
15901
|
+
pass
|
|
15902
|
+
try:
|
|
15903
|
+
log_handle.write(chunk)
|
|
15904
|
+
except Exception:
|
|
15905
|
+
pass
|
|
15906
|
+
finally:
|
|
15907
|
+
try:
|
|
15908
|
+
log_handle.close()
|
|
15909
|
+
except Exception:
|
|
15910
|
+
pass
|
|
15911
|
+
|
|
15912
|
+
tee_thread = threading.Thread(target=_tee_stderr, daemon=True, name="claude-stderr-tee")
|
|
15913
|
+
tee_thread.start()
|
|
15914
|
+
rc = proc.wait()
|
|
15915
|
+
tee_thread.join(timeout=2.0)
|
|
15916
|
+
router_log("INFO", f"claude_exit code={rc} stderr_log={CLAUDE_CODE_STDERR_LOG}")
|
|
15917
|
+
return rc
|
|
15918
|
+
|
|
15919
|
+
|
|
15460
15920
|
def cli_usage() -> str:
|
|
15461
15921
|
return """Usage:
|
|
15462
15922
|
claude-any Launch Claude Code through claude-any router
|
package/package.json
CHANGED