@oneciel-ai/claude-any 0.1.79 → 0.1.81

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 +153 -12
  2. package/package.json +1 -1
package/claude_any.py CHANGED
@@ -104,7 +104,7 @@ OFFICIAL_CHANNEL_PLUGINS = {
104
104
  "fakechat": "plugin:fakechat@claude-plugins-official",
105
105
  }
106
106
  APP_NAME = "Claude Any"
107
- VERSION = "0.1.79"
107
+ VERSION = "0.1.81"
108
108
  CREDITS = "Credits: One Ciel LLC"
109
109
 
110
110
  LOG_LEVELS = {"SILENT": 0, "ERROR": 1, "WARN": 2, "INFO": 3, "DEBUG": 4, "TRACE": 5}
@@ -4481,14 +4481,35 @@ def _event_meta_from_sources(*sources: Any) -> dict[str, Any]:
4481
4481
  "thread_id",
4482
4482
  "parent_id",
4483
4483
  "message_id",
4484
+ "event_id",
4485
+ "cursor",
4486
+ "sequence",
4487
+ "seq",
4484
4488
  "task_id",
4485
4489
  "round_id",
4490
+ "conversation_id",
4491
+ "session_id",
4486
4492
  "agent_id",
4493
+ "agent_name",
4487
4494
  "sender_id",
4495
+ "sender",
4488
4496
  "recipient_id",
4497
+ "recipient",
4498
+ "recipients",
4499
+ "target_id",
4500
+ "target",
4489
4501
  "type",
4490
4502
  "event_type",
4491
4503
  "kind",
4504
+ "timestamp",
4505
+ "created_at",
4506
+ "updated_at",
4507
+ "status",
4508
+ "priority",
4509
+ "title",
4510
+ "name",
4511
+ "model",
4512
+ "runtime",
4492
4513
  ):
4493
4514
  value = source.get(key)
4494
4515
  if value is not None and key not in meta:
@@ -4496,7 +4517,45 @@ def _event_meta_from_sources(*sources: Any) -> dict[str, Any]:
4496
4517
  return meta
4497
4518
 
4498
4519
 
4499
- def _sse_payload_to_chat_payload(data_text: str, event_name: str, defaults: dict[str, Any]) -> dict[str, Any] | None:
4520
+ def _metadata_key_is_sensitive(key: str) -> bool:
4521
+ return bool(re.search(r"(authorization|api[_-]?key|token|secret|password|credential|cookie)", key, re.I))
4522
+
4523
+
4524
+ def _json_safe_metadata(value: Any, depth: int = 0) -> Any:
4525
+ if depth > 8:
4526
+ return "<max-depth>"
4527
+ if value is None or isinstance(value, (bool, int, float)):
4528
+ return value
4529
+ if isinstance(value, str):
4530
+ return value if len(value) <= 8000 else value[:8000] + "...<truncated>"
4531
+ if isinstance(value, list):
4532
+ out = [_json_safe_metadata(item, depth + 1) for item in value[:200]]
4533
+ if len(value) > 200:
4534
+ out.append(f"...<{len(value) - 200} more>")
4535
+ return out
4536
+ if isinstance(value, dict):
4537
+ out: dict[str, Any] = {}
4538
+ items = list(value.items())
4539
+ for key, item in items[:200]:
4540
+ skey = str(key)
4541
+ out[skey] = "[redacted]" if _metadata_key_is_sensitive(skey) else _json_safe_metadata(item, depth + 1)
4542
+ if len(items) > 200:
4543
+ out["..."] = f"<{len(items) - 200} more>"
4544
+ return out
4545
+ return str(value)
4546
+
4547
+
4548
+ def _compact_json_for_prompt(value: Any, max_chars: int = 2400) -> str:
4549
+ try:
4550
+ text = json.dumps(value, ensure_ascii=False, separators=(",", ":"), default=str)
4551
+ except Exception:
4552
+ text = str(value)
4553
+ if len(text) <= max_chars:
4554
+ return text
4555
+ return text[: max_chars - 16] + "...<truncated>"
4556
+
4557
+
4558
+ def _sse_payload_to_chat_payload(data_text: str, event_name: str, defaults: dict[str, Any], event_id: str | None = None) -> dict[str, Any] | None:
4500
4559
  text = (data_text or "").strip()
4501
4560
  if not text or text == "[DONE]":
4502
4561
  return None
@@ -4512,6 +4571,8 @@ def _sse_payload_to_chat_payload(data_text: str, event_name: str, defaults: dict
4512
4571
  "sse_event": event_name or "message",
4513
4572
  "sse_source": defaults.get("name") or "",
4514
4573
  }
4574
+ if event_id:
4575
+ meta["sse_id"] = event_id
4515
4576
  content = text
4516
4577
  kind = "sse"
4517
4578
  method = str(event_name or "message")
@@ -4519,6 +4580,13 @@ def _sse_payload_to_chat_payload(data_text: str, event_name: str, defaults: dict
4519
4580
  allowed_events = {str(item).strip() for item in event_filter if str(item).strip()} if isinstance(event_filter, list) else set()
4520
4581
  if isinstance(parsed, dict):
4521
4582
  method = str(parsed.get("method") or event_name or "message")
4583
+ meta["sse_json"] = _json_safe_metadata(parsed)
4584
+ if parsed.get("jsonrpc") is not None:
4585
+ meta["jsonrpc"] = parsed.get("jsonrpc")
4586
+ if parsed.get("id") is not None:
4587
+ meta["rpc_id"] = parsed.get("id")
4588
+ if parsed.get("method") is not None:
4589
+ meta["mcp_method"] = parsed.get("method")
4522
4590
  params = parsed.get("params") if isinstance(parsed.get("params"), dict) else {}
4523
4591
  payload = parsed.get("payload") if isinstance(parsed.get("payload"), dict) else {}
4524
4592
  data = params.get("data") if isinstance(params.get("data"), dict) else {}
@@ -4614,7 +4682,7 @@ def _channel_sse_maybe_initialize_mcp(name: str, endpoint_text: str) -> None:
4614
4682
  router_log("WARN", f"channel_sse_mcp_initialize_failed name={name} endpoint={endpoint} error={type(exc).__name__}: {exc}")
4615
4683
 
4616
4684
 
4617
- def _channel_sse_dispatch(name: str, event_name: str, data_lines: list[str]) -> None:
4685
+ def _channel_sse_dispatch(name: str, event_name: str, data_lines: list[str], event_id: str | None = None) -> None:
4618
4686
  data_text = "\n".join(data_lines)
4619
4687
  if (event_name or "").strip().lower() == "endpoint":
4620
4688
  _channel_sse_maybe_initialize_mcp(name, data_text)
@@ -4624,10 +4692,10 @@ def _channel_sse_dispatch(name: str, event_name: str, data_lines: list[str]) ->
4624
4692
  if not state:
4625
4693
  return
4626
4694
  defaults = dict(state)
4627
- payload = _sse_payload_to_chat_payload(data_text, event_name, defaults)
4695
+ payload = _sse_payload_to_chat_payload(data_text, event_name, defaults, event_id=event_id)
4628
4696
  if not payload:
4629
4697
  return
4630
- append_chat_message(payload)
4698
+ saved = append_chat_message(payload)
4631
4699
  now = time.strftime("%Y-%m-%dT%H:%M:%S")
4632
4700
  with _CHANNEL_SSE_LOCK:
4633
4701
  state = _CHANNEL_SSE_CONNECTIONS.get(name)
@@ -4635,6 +4703,10 @@ def _channel_sse_dispatch(name: str, event_name: str, data_lines: list[str]) ->
4635
4703
  state["last_event_at"] = now
4636
4704
  state["messages_received"] = int(state.get("messages_received") or 0) + 1
4637
4705
  state["last_error"] = None
4706
+ router_log(
4707
+ "INFO",
4708
+ f"channel_sse_message_received name={name} event={event_name or 'message'} message_id={saved.get('id')} channel={saved.get('channel')}",
4709
+ )
4638
4710
 
4639
4711
 
4640
4712
  def _channel_sse_worker(name: str) -> None:
@@ -4648,10 +4720,13 @@ def _channel_sse_worker(name: str) -> None:
4648
4720
  read_timeout = max(5.0, min(3600.0, float(state.get("read_timeout_seconds") or 300.0)))
4649
4721
  retry_seconds = max(1.0, min(60.0, float(state.get("retry_seconds") or 5.0)))
4650
4722
  event_name = "message"
4723
+ event_id: str | None = None
4651
4724
  data_lines: list[str] = []
4652
4725
  try:
4653
4726
  req = urllib.request.Request(url, headers={**headers, "Accept": "text/event-stream"})
4654
4727
  with urllib.request.urlopen(req, timeout=read_timeout) as response:
4728
+ _channel_sse_set_state(name, last_error=None)
4729
+ router_log("INFO", f"channel_sse_connected name={name} url={url}")
4655
4730
  while True:
4656
4731
  with _CHANNEL_SSE_LOCK:
4657
4732
  current = _CHANNEL_SSE_CONNECTIONS.get(name)
@@ -4663,8 +4738,9 @@ def _channel_sse_worker(name: str) -> None:
4663
4738
  line = raw.decode("utf-8", errors="replace").rstrip("\r\n")
4664
4739
  if not line:
4665
4740
  if data_lines:
4666
- _channel_sse_dispatch(name, event_name, data_lines)
4741
+ _channel_sse_dispatch(name, event_name, data_lines, event_id=event_id)
4667
4742
  event_name = "message"
4743
+ event_id = None
4668
4744
  data_lines = []
4669
4745
  continue
4670
4746
  if line.startswith(":"):
@@ -4676,6 +4752,8 @@ def _channel_sse_worker(name: str) -> None:
4676
4752
  event_name = value or "message"
4677
4753
  elif field == "data":
4678
4754
  data_lines.append(value)
4755
+ elif field == "id":
4756
+ event_id = value
4679
4757
  elif field == "retry":
4680
4758
  try:
4681
4759
  retry_seconds = max(1.0, min(60.0, int(value) / 1000.0))
@@ -4687,6 +4765,7 @@ def _channel_sse_worker(name: str) -> None:
4687
4765
  if not state or not state.get("running"):
4688
4766
  return
4689
4767
  state["last_error"] = f"{type(exc).__name__}: {exc}"
4768
+ router_log("WARN", f"channel_sse_reconnect name={name} error={type(exc).__name__}: {exc}")
4690
4769
  time.sleep(retry_seconds)
4691
4770
 
4692
4771
 
@@ -13612,14 +13691,57 @@ def format_channel_wake_prompt(message: dict[str, Any]) -> str:
13612
13691
  fields.append(f"id={mid}")
13613
13692
  if thread:
13614
13693
  fields.append(f"thread={thread}")
13694
+ meta_text = f" metadata={_compact_json_for_prompt(meta)}" if meta else ""
13615
13695
  return (
13616
13696
  "[claude-any external channel message] "
13617
13697
  + " ".join(fields)
13618
- + f" text={json.dumps(body, ensure_ascii=False)}. "
13698
+ + f" text={json.dumps(body, ensure_ascii=False)}"
13699
+ + meta_text
13700
+ + ". "
13619
13701
  + "If relevant to current work, respond or act now; otherwise keep working."
13620
13702
  )
13621
13703
 
13622
13704
 
13705
+ def _channel_wake_message_noise_reason(message: dict[str, Any]) -> str | None:
13706
+ body = re.sub(r"\s+", " ", str(message.get("message") or "")).strip().lower()
13707
+ kind = str(message.get("kind") or "").strip().lower()
13708
+ if not body:
13709
+ return "empty"
13710
+ if kind in {"connection", "connected", "heartbeat", "keepalive"}:
13711
+ return kind
13712
+ if re.fullmatch(r"[a-z0-9_.:-]{1,80}\.(ws|sse)\.connected", body):
13713
+ return "transport_connected"
13714
+ return None
13715
+
13716
+
13717
+ def _channel_wake_message_is_noise(message: dict[str, Any]) -> bool:
13718
+ return _channel_wake_message_noise_reason(message) is not None
13719
+
13720
+
13721
+ def format_channel_wake_batch_prompt(messages: list[dict[str, Any]]) -> str:
13722
+ if len(messages) == 1:
13723
+ return format_channel_wake_prompt(messages[0])
13724
+ parts: list[str] = []
13725
+ for message in messages:
13726
+ channel = str(message.get("channel") or "default")
13727
+ sender = str(message.get("sender_id") or "channel")
13728
+ mid = str(message.get("id") or "")
13729
+ meta = message.get("meta") if isinstance(message.get("meta"), dict) else {}
13730
+ room = str(meta.get("room_id") or meta.get("room") or channel)
13731
+ thread = str(message.get("thread_id") or meta.get("thread_id") or "")
13732
+ body = re.sub(r"\s+", " ", str(message.get("message") or "")).strip()
13733
+ fields = [f"id={mid}", f"room={room}", f"from={sender}"]
13734
+ if thread:
13735
+ fields.append(f"thread={thread}")
13736
+ meta_text = f" metadata={_compact_json_for_prompt(meta)}" if meta else ""
13737
+ parts.append("(" + " ".join(fields) + ") " + json.dumps(body, ensure_ascii=False) + meta_text)
13738
+ return (
13739
+ f"[claude-any external channel messages] {len(messages)} new messages: "
13740
+ + " ; ".join(parts)
13741
+ + ". If relevant to current work, respond or act now; otherwise keep working."
13742
+ )
13743
+
13744
+
13623
13745
  def _write_fd_all(fd: int, data: bytes) -> None:
13624
13746
  view = memoryview(data)
13625
13747
  while view:
@@ -13629,18 +13751,32 @@ def _write_fd_all(fd: int, data: bytes) -> None:
13629
13751
 
13630
13752
  def _channel_wake_input_bytes(prompt: str) -> bytes:
13631
13753
  # Ctrl-U clears any stale line editor text before submitting the synthetic prompt.
13632
- return b"\x15" + prompt.encode("utf-8", errors="replace") + b"\n"
13754
+ # Claude Code's interactive input is terminal-driven; send carriage return,
13755
+ # the same byte a TTY normally delivers for Enter in raw mode.
13756
+ return b"\x15" + prompt.encode("utf-8", errors="replace") + b"\r"
13633
13757
 
13634
13758
 
13635
13759
  def _inject_pending_channel_messages(master_fd: int, last_id: int) -> int:
13760
+ pending: list[dict[str, Any]] = []
13636
13761
  for message in read_chat_messages(last_id, None, None, 100):
13637
13762
  try:
13638
13763
  last_id = max(last_id, int(message.get("id") or 0))
13639
13764
  except Exception:
13640
13765
  continue
13641
- prompt = format_channel_wake_prompt(message)
13766
+ noise_reason = _channel_wake_message_noise_reason(message)
13767
+ if noise_reason:
13768
+ router_log(
13769
+ "INFO",
13770
+ f"channel_stdin_proxy_skipped_noise message_id={message.get('id')} channel={message.get('channel')} reason={noise_reason}",
13771
+ )
13772
+ continue
13773
+ pending.append(message)
13774
+ if pending:
13775
+ prompt = format_channel_wake_batch_prompt(pending)
13642
13776
  _write_fd_all(master_fd, _channel_wake_input_bytes(prompt))
13643
- router_log("INFO", f"channel_stdin_proxy_injected message_id={message.get('id')} channel={message.get('channel')}")
13777
+ ids = ",".join(str(message.get("id") or "") for message in pending)
13778
+ channels = ",".join(sorted({str(message.get("channel") or "default") for message in pending}))
13779
+ router_log("INFO", f"channel_stdin_proxy_injected count={len(pending)} message_ids={ids} channels={channels}")
13644
13780
  return last_id
13645
13781
 
13646
13782
 
@@ -13661,15 +13797,15 @@ def subprocess_call_with_channel_wake_proxy(cmd: list[str], env: dict[str, str])
13661
13797
  import termios
13662
13798
  import tty
13663
13799
 
13800
+ last_id = _chat_init_next_id() - 1
13801
+ last_channel_marker = _chat_messages_file_marker()
13664
13802
  master_fd, slave_fd = pty.openpty()
13665
13803
  proc = subprocess.Popen(cmd, stdin=slave_fd, stdout=slave_fd, stderr=slave_fd, env=env, close_fds=True)
13666
13804
  os.close(slave_fd)
13667
13805
  stdin_fd = sys.stdin.fileno()
13668
13806
  stdout_fd = sys.stdout.fileno()
13669
13807
  old_attrs = termios.tcgetattr(stdin_fd)
13670
- last_id = _chat_init_next_id() - 1
13671
13808
  last_channel_poll = 0.0
13672
- last_channel_marker = _chat_messages_file_marker()
13673
13809
  try:
13674
13810
  tty.setraw(stdin_fd)
13675
13811
  while proc.poll() is None:
@@ -13734,7 +13870,12 @@ def _mcp_proxy_notification_payload(server_name: str, message: dict[str, Any]) -
13734
13870
  meta: dict[str, Any] = {
13735
13871
  "mcp_server": server_name,
13736
13872
  "mcp_method": method,
13873
+ "mcp_json": _json_safe_metadata(message),
13737
13874
  }
13875
+ if message.get("jsonrpc") is not None:
13876
+ meta["jsonrpc"] = message.get("jsonrpc")
13877
+ if message.get("id") is not None:
13878
+ meta["rpc_id"] = message.get("id")
13738
13879
  meta.update(_event_meta_from_sources(message, params, payload, data, event))
13739
13880
  content = (
13740
13881
  _event_payload_text(params)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneciel-ai/claude-any",
3
- "version": "0.1.79",
3
+ "version": "0.1.81",
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",