@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.
- package/claude_any.py +153 -12
- 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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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