@oneciel-ai/claude-any 0.1.86 → 0.1.88

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 +89 -10
  2. package/package.json +1 -1
package/claude_any.py CHANGED
@@ -58,6 +58,7 @@ MODEL_LIST_CACHE_PATH = CONFIG_DIR / "model-list-cache.json"
58
58
  WEB_TOOLS_MCP_CONFIG = CONFIG_DIR / "web-tools-mcp.json"
59
59
  DUCKDUCKGO_MCP_CONFIG = CONFIG_DIR / "duckduckgo-mcp.json"
60
60
  CHANNEL_MCP_CONFIG = CONFIG_DIR / "channel-mcp.json"
61
+ CHANNEL_MCP_CURSOR_PATH = CONFIG_DIR / "channel-mcp-cursor.json"
61
62
  MCP_PROXY_CONFIG = CONFIG_DIR / "mcp-proxy.json"
62
63
  ROUTER_HOST = os.environ.get("CLAUDE_ANY_ROUTER_CLIENT_HOST", "127.0.0.1").strip() or "127.0.0.1"
63
64
  ROUTER_PORT = 8799
@@ -104,7 +105,7 @@ OFFICIAL_CHANNEL_PLUGINS = {
104
105
  "fakechat": "plugin:fakechat@claude-plugins-official",
105
106
  }
106
107
  APP_NAME = "Claude Any"
107
- VERSION = "0.1.86"
108
+ VERSION = "0.1.88"
108
109
  CREDITS = "Credits: One Ciel LLC"
109
110
 
110
111
  LOG_LEVELS = {"SILENT": 0, "ERROR": 1, "WARN": 2, "INFO": 3, "DEBUG": 4, "TRACE": 5}
@@ -124,6 +125,8 @@ _CHANNEL_SSE_LOCK = threading.Lock()
124
125
  _CHANNEL_SSE_CONNECTIONS: dict[str, dict[str, Any]] = {}
125
126
  _CHANNEL_MCP_LOCK = threading.Lock()
126
127
  _CHANNEL_MCP_SESSIONS: dict[str, dict[str, Any]] = {}
128
+ _CHANNEL_MCP_CURSOR_LOCK = threading.Lock()
129
+ _CHANNEL_MCP_CURSOR_LAST_ID: int | None = None
127
130
  _NATIVE_CHANNEL_NOTIFICATION_METHOD = "notifications/claude/channel"
128
131
  _MCP_NOTIFICATION_DEDUP_TTL_SECONDS = 3.0
129
132
  _MCP_NOTIFICATION_DEDUP_LOCK = threading.Lock()
@@ -4917,6 +4920,61 @@ def _write_sse_event(handler: BaseHTTPRequestHandler, event: str, data: Any, eve
4917
4920
  handler.wfile.flush()
4918
4921
 
4919
4922
 
4923
+ def _send_channel_mcp_sse_headers(handler: BaseHTTPRequestHandler) -> None:
4924
+ handler.send_response(200)
4925
+ handler.send_header("content-type", "text/event-stream")
4926
+ handler.send_header("cache-control", "no-cache, no-transform")
4927
+ handler.send_header("connection", "keep-alive")
4928
+ handler.send_header("x-accel-buffering", "no")
4929
+ handler.end_headers()
4930
+
4931
+
4932
+ def _channel_mcp_write_cursor_locked(last_id: int) -> None:
4933
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
4934
+ tmp_path = CHANNEL_MCP_CURSOR_PATH.with_suffix(".json.tmp")
4935
+ tmp_path.write_text(json.dumps({"last_id": max(0, int(last_id))}, separators=(",", ":")) + "\n", encoding="utf-8")
4936
+ tmp_path.replace(CHANNEL_MCP_CURSOR_PATH)
4937
+
4938
+
4939
+ def _channel_mcp_read_cursor_locked() -> int:
4940
+ global _CHANNEL_MCP_CURSOR_LAST_ID
4941
+ if _CHANNEL_MCP_CURSOR_LAST_ID is not None:
4942
+ return _CHANNEL_MCP_CURSOR_LAST_ID
4943
+ if CHANNEL_MCP_CURSOR_PATH.exists():
4944
+ try:
4945
+ data = json.loads(CHANNEL_MCP_CURSOR_PATH.read_text(encoding="utf-8"))
4946
+ _CHANNEL_MCP_CURSOR_LAST_ID = max(0, int(data.get("last_id") or 0))
4947
+ return _CHANNEL_MCP_CURSOR_LAST_ID
4948
+ except Exception as exc:
4949
+ router_log("WARN", f"channel_mcp_cursor_read_failed error={type(exc).__name__}: {exc}")
4950
+ _CHANNEL_MCP_CURSOR_LAST_ID = max(0, _chat_init_next_id() - 1)
4951
+ try:
4952
+ _channel_mcp_write_cursor_locked(_CHANNEL_MCP_CURSOR_LAST_ID)
4953
+ except Exception as exc:
4954
+ router_log("WARN", f"channel_mcp_cursor_write_failed error={type(exc).__name__}: {exc}")
4955
+ return _CHANNEL_MCP_CURSOR_LAST_ID
4956
+
4957
+
4958
+ def _channel_mcp_ensure_cursor_initialized() -> int:
4959
+ with _CHANNEL_MCP_CURSOR_LOCK:
4960
+ return _channel_mcp_read_cursor_locked()
4961
+
4962
+
4963
+ def _channel_mcp_update_cursor(last_id: int) -> None:
4964
+ global _CHANNEL_MCP_CURSOR_LAST_ID
4965
+ if last_id < 0:
4966
+ return
4967
+ with _CHANNEL_MCP_CURSOR_LOCK:
4968
+ current = _channel_mcp_read_cursor_locked()
4969
+ if last_id <= current:
4970
+ return
4971
+ _CHANNEL_MCP_CURSOR_LAST_ID = int(last_id)
4972
+ try:
4973
+ _channel_mcp_write_cursor_locked(_CHANNEL_MCP_CURSOR_LAST_ID)
4974
+ except Exception as exc:
4975
+ router_log("WARN", f"channel_mcp_cursor_write_failed error={type(exc).__name__}: {exc}")
4976
+
4977
+
4920
4978
  def _channel_mcp_notifications_for_messages(
4921
4979
  messages: list[dict[str, Any]],
4922
4980
  session: str = "",
@@ -4948,28 +5006,36 @@ def handle_channel_mcp_get(handler: BaseHTTPRequestHandler, path: str) -> bool:
4948
5006
  if path != "/ca/mcp/sse":
4949
5007
  return False
4950
5008
  session = _channel_mcp_session_id()
4951
- last_id = _chat_init_next_id() - 1
5009
+ last_id = _channel_mcp_ensure_cursor_initialized()
4952
5010
  with _CHANNEL_MCP_LOCK:
4953
5011
  _CHANNEL_MCP_SESSIONS[session] = {"created_at": time.time(), "last_id": last_id, "initialized": False}
4954
- handler.send_response(200)
4955
- handler.send_header("content-type", "text/event-stream")
4956
- handler.send_header("cache-control", "no-cache")
4957
- handler.send_header("connection", "close")
4958
- handler.end_headers()
5012
+ router_log("INFO", f"channel_mcp_session_started session={session} last_id={last_id}")
5013
+ started_at = time.time()
5014
+ close_reason = "finished"
5015
+ _send_channel_mcp_sse_headers(handler)
4959
5016
  _write_sse_event(handler, "endpoint", f"/ca/mcp/messages?session={urllib.parse.quote(session)}")
4960
5017
  try:
4961
5018
  while True:
4962
5019
  with _CHANNEL_MCP_LOCK:
4963
5020
  state = _CHANNEL_MCP_SESSIONS.get(session)
4964
5021
  if not state:
5022
+ close_reason = "session_missing"
4965
5023
  return True
4966
5024
  last_id = int(state.get("last_id") or 0)
5025
+ initialized = bool(state.get("initialized"))
5026
+ if not initialized:
5027
+ handler.wfile.write(b": waiting-for-initialize\n\n")
5028
+ handler.wfile.flush()
5029
+ with _CHAT_CONDITION:
5030
+ _CHAT_CONDITION.wait(timeout=1.0)
5031
+ continue
4967
5032
  messages = read_chat_messages(last_id, None, None, 100)
4968
5033
  if messages:
4969
5034
  delivered_last_id, events = _channel_mcp_notifications_for_messages(messages, session)
4970
5035
  last_id = max(last_id, delivered_last_id)
4971
5036
  for event_id, notification in events:
4972
5037
  _write_sse_event(handler, "message", notification, event_id)
5038
+ _channel_mcp_update_cursor(last_id)
4973
5039
  with _CHANNEL_MCP_LOCK:
4974
5040
  state = _CHANNEL_MCP_SESSIONS.get(session)
4975
5041
  if state:
@@ -4978,10 +5044,16 @@ def handle_channel_mcp_get(handler: BaseHTTPRequestHandler, path: str) -> bool:
4978
5044
  handler.wfile.write(b": keepalive\n\n")
4979
5045
  handler.wfile.flush()
4980
5046
  with _CHAT_CONDITION:
4981
- _CHAT_CONDITION.wait(timeout=15.0)
4982
- except (BrokenPipeError, ConnectionError, ConnectionResetError):
5047
+ _CHAT_CONDITION.wait(timeout=5.0)
5048
+ except (BrokenPipeError, ConnectionError, ConnectionResetError) as exc:
5049
+ close_reason = type(exc).__name__
5050
+ return True
5051
+ except Exception as exc:
5052
+ close_reason = type(exc).__name__
5053
+ router_log("ERROR", f"channel_mcp_session_failed session={session} error={type(exc).__name__}: {exc}")
4983
5054
  return True
4984
5055
  finally:
5056
+ router_log("INFO", f"channel_mcp_session_closed session={session} reason={close_reason} age={time.time() - started_at:.1f}s")
4985
5057
  with _CHANNEL_MCP_LOCK:
4986
5058
  _CHANNEL_MCP_SESSIONS.pop(session, None)
4987
5059
 
@@ -5004,6 +5076,8 @@ def handle_channel_mcp_post(handler: BaseHTTPRequestHandler, path: str, body: di
5004
5076
  with _CHANNEL_MCP_LOCK:
5005
5077
  if session and session in _CHANNEL_MCP_SESSIONS:
5006
5078
  _CHANNEL_MCP_SESSIONS[session]["initialized"] = True
5079
+ with _CHAT_CONDITION:
5080
+ _CHAT_CONDITION.notify_all()
5007
5081
  write_json(
5008
5082
  handler,
5009
5083
  {
@@ -13767,6 +13841,7 @@ def write_channel_mcp_config() -> Path:
13767
13841
  os.chmod(CHANNEL_MCP_CONFIG, 0o600)
13768
13842
  except Exception:
13769
13843
  pass
13844
+ _channel_mcp_ensure_cursor_initialized()
13770
13845
  return CHANNEL_MCP_CONFIG
13771
13846
 
13772
13847
 
@@ -14326,9 +14401,11 @@ def run_mcp_stdio_proxy(server_name: str, server_config_path: Path) -> int:
14326
14401
  try:
14327
14402
  server = json.loads(server_config_path.read_text(encoding="utf-8"))
14328
14403
  except Exception as exc:
14404
+ router_log("ERROR", f"mcp_proxy_config_read_failed server={server_name} error={type(exc).__name__}: {exc}")
14329
14405
  print(f"claude-any mcp-proxy: cannot read server config: {type(exc).__name__}: {exc}", file=sys.stderr, flush=True)
14330
14406
  return 2
14331
14407
  if not isinstance(server, dict) or not _mcp_server_is_stdio(server):
14408
+ router_log("ERROR", f"mcp_proxy_invalid_config server={server_name}")
14332
14409
  print("claude-any mcp-proxy: server config is not a stdio MCP server", file=sys.stderr, flush=True)
14333
14410
  return 2
14334
14411
  command = str(server.get("command") or "").strip()
@@ -14350,6 +14427,7 @@ def run_mcp_stdio_proxy(server_name: str, server_config_path: Path) -> int:
14350
14427
  bufsize=0,
14351
14428
  )
14352
14429
  except Exception as exc:
14430
+ router_log("ERROR", f"mcp_proxy_start_failed server={server_name} command={command} error={type(exc).__name__}: {exc}")
14353
14431
  print(f"claude-any mcp-proxy: failed to start {command}: {type(exc).__name__}: {exc}", file=sys.stderr, flush=True)
14354
14432
  return 127
14355
14433
  router_log("INFO", f"mcp_proxy_started server={server_name} command={command}")
@@ -14366,7 +14444,8 @@ def run_mcp_stdio_proxy(server_name: str, server_config_path: Path) -> int:
14366
14444
  sys.stdout.buffer.write(chunk)
14367
14445
  sys.stdout.buffer.flush()
14368
14446
  rc = proc.wait()
14369
- router_log("INFO", f"mcp_proxy_exited server={server_name} rc={rc}")
14447
+ level = "INFO" if rc == 0 else "WARN"
14448
+ router_log(level, f"mcp_proxy_exited server={server_name} rc={rc}")
14370
14449
  return rc
14371
14450
  finally:
14372
14451
  if proc.poll() is None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneciel-ai/claude-any",
3
- "version": "0.1.86",
3
+ "version": "0.1.88",
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",