@oneciel-ai/claude-any 0.1.70 → 0.1.72
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/README.md +9 -1
- package/claude_any.py +508 -162
- package/docs/README.ja.md +9 -1
- package/docs/README.ko.md +9 -1
- package/docs/README.zh.md +9 -1
- package/docs/manual.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ arguments through unchanged.
|
|
|
68
68
|
|
|
69
69
|
Credits: One Ciel LLC
|
|
70
70
|
|
|
71
|
-
Current version: `0.1.
|
|
71
|
+
Current version: `0.1.71`
|
|
72
72
|
|
|
73
73
|
## Why This Exists
|
|
74
74
|
|
|
@@ -495,6 +495,14 @@ steps under that larger model's supervision.
|
|
|
495
495
|
|
|
496
496
|
## Changelog
|
|
497
497
|
|
|
498
|
+
### 0.1.71
|
|
499
|
+
|
|
500
|
+
- **MCP SSE channel initialization**: the channel bridge now handles MCP
|
|
501
|
+
`endpoint` events by sending `initialize` and `notifications/initialized`,
|
|
502
|
+
so AI-Net style push notifications can start flowing into Claude Any.
|
|
503
|
+
- **Channel SSE diagnostics**: connector status now reports the MCP endpoint,
|
|
504
|
+
initialization state, and last MCP initialization error.
|
|
505
|
+
|
|
498
506
|
### 0.1.70
|
|
499
507
|
|
|
500
508
|
- **Linux menu debug log fix**: key-debug logging now writes under the user's
|
package/claude_any.py
CHANGED
|
@@ -95,8 +95,14 @@ PROVIDER_LABELS = {
|
|
|
95
95
|
"nvidia-hosted": "Nvidia Hosted",
|
|
96
96
|
"self-hosted-nim": "Self Hosted NIM",
|
|
97
97
|
}
|
|
98
|
+
OFFICIAL_CHANNEL_PLUGINS = {
|
|
99
|
+
"telegram": "plugin:telegram@claude-plugins-official",
|
|
100
|
+
"discord": "plugin:discord@claude-plugins-official",
|
|
101
|
+
"imessage": "plugin:imessage@claude-plugins-official",
|
|
102
|
+
"fakechat": "plugin:fakechat@claude-plugins-official",
|
|
103
|
+
}
|
|
98
104
|
APP_NAME = "Claude Any"
|
|
99
|
-
VERSION = "0.1.
|
|
105
|
+
VERSION = "0.1.71"
|
|
100
106
|
CREDITS = "Credits: One Ciel LLC"
|
|
101
107
|
|
|
102
108
|
LOG_LEVELS = {"SILENT": 0, "ERROR": 1, "WARN": 2, "INFO": 3, "DEBUG": 4, "TRACE": 5}
|
|
@@ -116,7 +122,6 @@ _CHANNEL_SSE_LOCK = threading.Lock()
|
|
|
116
122
|
_CHANNEL_SSE_CONNECTIONS: dict[str, dict[str, Any]] = {}
|
|
117
123
|
EVENT_BUS = EventBus()
|
|
118
124
|
ADVISOR_FEEDBACK_MARKER = "CLAUDE_ANY_ADVISOR_FEEDBACK"
|
|
119
|
-
CHANNEL_BRIDGE_MARKER = "CLAUDE_ANY_CHANNEL_BRIDGE"
|
|
120
125
|
PLAN_GUARD_MARKER = "[claude-any-plan-guard]"
|
|
121
126
|
TASK_UPDATE_STATUSES = {"pending", "in_progress", "completed", "deleted"}
|
|
122
127
|
TASK_UPDATE_STATUS_ALIASES = {
|
|
@@ -189,8 +194,6 @@ NON_ANTHROPIC_COMPAT_PROMPT = (
|
|
|
189
194
|
"TaskList: no input. TaskUpdate: taskId (string), optional status enum exactly one of pending, in_progress, completed, deleted. "
|
|
190
195
|
"CronCreate: cron (standard 5-field local-time cron string), prompt (string), optional recurring (boolean), optional durable (boolean). "
|
|
191
196
|
"CronDelete: id (string returned by CronCreate). CronList: no input. "
|
|
192
|
-
"Claude Any channel bridge: external systems can post messages to /ca/channel/messages or /ca/channel/notify. "
|
|
193
|
-
"Use /channel status, /channel poll, or /channel wait to inspect bridge messages when the user asks about external channels. "
|
|
194
197
|
"Never write pseudo tool calls, partial JSON, or markdown code fences when a real Claude Code tool call is required."
|
|
195
198
|
)
|
|
196
199
|
LANGUAGES = {
|
|
@@ -1248,6 +1251,8 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|
|
1248
1251
|
"router_debug_message_preview_chars": 0,
|
|
1249
1252
|
"claude_code": {
|
|
1250
1253
|
"compat_prompt_for_non_anthropic": True,
|
|
1254
|
+
"channels": [],
|
|
1255
|
+
"development_channels": False,
|
|
1251
1256
|
},
|
|
1252
1257
|
"cleanup": {
|
|
1253
1258
|
"managed_services_on_launch": True,
|
|
@@ -2173,32 +2178,21 @@ Value: $ARGUMENTS
|
|
|
2173
2178
|
Toggle claude-any router debug external access. With no argument, this toggles the current state. Use `on`, `off`, or `status` for explicit control.
|
|
2174
2179
|
"""
|
|
2175
2180
|
|
|
2176
|
-
CHANNEL_SLASH_COMMAND = """---
|
|
2177
|
-
description: Inspect or send messages through the claude-any channel bridge
|
|
2178
|
-
argument-hint: [status|poll|wait|send|sse key=value ...]
|
|
2179
|
-
---
|
|
2180
|
-
|
|
2181
|
-
CLAUDE_ANY_CHANNEL_BRIDGE
|
|
2182
|
-
|
|
2183
|
-
Args: $ARGUMENTS
|
|
2184
|
-
|
|
2185
|
-
Use the claude-any channel bridge for external agent/channel messages. Examples:
|
|
2186
|
-
- `/channel status`
|
|
2187
|
-
- `/channel poll channel=default after=0 recipient=claude`
|
|
2188
|
-
- `/channel wait channel=default after=12 timeout=60`
|
|
2189
|
-
- `/channel send channel=default to=all message="hello"`
|
|
2190
|
-
- `/channel sse`
|
|
2191
|
-
"""
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
2181
|
def install_claude_any_slash_commands() -> None:
|
|
2195
2182
|
try:
|
|
2196
2183
|
CLAUDE_COMMANDS_DIR.mkdir(parents=True, exist_ok=True)
|
|
2197
2184
|
commands = {
|
|
2198
2185
|
"advisor.md": ADVISOR_SLASH_COMMAND,
|
|
2199
2186
|
"router-debug.md": ROUTER_DEBUG_SLASH_COMMAND,
|
|
2200
|
-
"channel.md": CHANNEL_SLASH_COMMAND,
|
|
2201
2187
|
}
|
|
2188
|
+
stale_channel = CLAUDE_COMMANDS_DIR / "channel.md"
|
|
2189
|
+
if stale_channel.exists():
|
|
2190
|
+
try:
|
|
2191
|
+
stale_text = stale_channel.read_text(encoding="utf-8", errors="replace")
|
|
2192
|
+
if "CLAUDE_ANY_CHANNEL_BRIDGE" in stale_text or "claude-any channel bridge" in stale_text:
|
|
2193
|
+
stale_channel.unlink()
|
|
2194
|
+
except Exception:
|
|
2195
|
+
pass
|
|
2202
2196
|
for name, content in commands.items():
|
|
2203
2197
|
path = CLAUDE_COMMANDS_DIR / name
|
|
2204
2198
|
if path.exists() and path.read_text(encoding="utf-8") == content:
|
|
@@ -2468,7 +2462,7 @@ def router_event_message_preview(body: dict[str, Any], cfg: dict[str, Any] | Non
|
|
|
2468
2462
|
text = latest_user_text(body).strip()
|
|
2469
2463
|
if not text:
|
|
2470
2464
|
return {"message_preview_chars": limit, "message_preview": "", "message_preview_truncated": False}
|
|
2471
|
-
normalized = re.sub(r"\s+", " ", text)
|
|
2465
|
+
normalized = re.sub(r"\s+", " ", redact_sensitive_text(text))
|
|
2472
2466
|
truncated = len(normalized) > limit
|
|
2473
2467
|
return {
|
|
2474
2468
|
"message_preview_chars": limit,
|
|
@@ -4286,8 +4280,16 @@ def read_chat_messages(after_id: int = 0, channel: str | None = None, recipient:
|
|
|
4286
4280
|
continue
|
|
4287
4281
|
except Exception:
|
|
4288
4282
|
continue
|
|
4289
|
-
if channel
|
|
4290
|
-
|
|
4283
|
+
if channel:
|
|
4284
|
+
meta = item.get("meta") if isinstance(item.get("meta"), dict) else {}
|
|
4285
|
+
aliases = {
|
|
4286
|
+
str(item.get("channel") or ""),
|
|
4287
|
+
str(meta.get("room_id") or ""),
|
|
4288
|
+
str(meta.get("room") or ""),
|
|
4289
|
+
str(meta.get("channel") or ""),
|
|
4290
|
+
}
|
|
4291
|
+
if channel not in aliases:
|
|
4292
|
+
continue
|
|
4291
4293
|
if not _message_visible_to(item, recipient):
|
|
4292
4294
|
continue
|
|
4293
4295
|
messages.append(item)
|
|
@@ -4338,6 +4340,9 @@ def _channel_sse_status_public(name: str, state: dict[str, Any]) -> dict[str, An
|
|
|
4338
4340
|
"messages_received": int(state.get("messages_received") or 0),
|
|
4339
4341
|
"event_filter": state.get("event_filter") or [],
|
|
4340
4342
|
"read_timeout_seconds": state.get("read_timeout_seconds"),
|
|
4343
|
+
"mcp_endpoint": state.get("mcp_endpoint"),
|
|
4344
|
+
"mcp_initialized": bool(state.get("mcp_initialized")),
|
|
4345
|
+
"mcp_last_error": state.get("mcp_last_error"),
|
|
4341
4346
|
"last_error": state.get("last_error"),
|
|
4342
4347
|
}
|
|
4343
4348
|
|
|
@@ -4347,6 +4352,74 @@ def channel_sse_status() -> dict[str, Any]:
|
|
|
4347
4352
|
return {name: _channel_sse_status_public(name, state) for name, state in _CHANNEL_SSE_CONNECTIONS.items()}
|
|
4348
4353
|
|
|
4349
4354
|
|
|
4355
|
+
def _first_present_dict_value(*sources: Any, keys: tuple[str, ...]) -> Any:
|
|
4356
|
+
for source in sources:
|
|
4357
|
+
if not isinstance(source, dict):
|
|
4358
|
+
continue
|
|
4359
|
+
for key in keys:
|
|
4360
|
+
value = source.get(key)
|
|
4361
|
+
if value is not None:
|
|
4362
|
+
return value
|
|
4363
|
+
return None
|
|
4364
|
+
|
|
4365
|
+
|
|
4366
|
+
def _event_payload_text(value: Any, depth: int = 0) -> str | None:
|
|
4367
|
+
if value is None or depth > 5:
|
|
4368
|
+
return None
|
|
4369
|
+
if isinstance(value, str):
|
|
4370
|
+
return value.strip() or None
|
|
4371
|
+
if not isinstance(value, dict):
|
|
4372
|
+
return str(value)
|
|
4373
|
+
direct = _first_present_dict_value(value, keys=("content", "message", "text", "body", "summary"))
|
|
4374
|
+
if isinstance(direct, str) and direct.strip():
|
|
4375
|
+
return direct.strip()
|
|
4376
|
+
if isinstance(direct, dict):
|
|
4377
|
+
nested = _event_payload_text(direct, depth + 1)
|
|
4378
|
+
if nested:
|
|
4379
|
+
return nested
|
|
4380
|
+
for key in ("data", "event", "payload", "message", "notification", "item"):
|
|
4381
|
+
nested = _event_payload_text(value.get(key), depth + 1)
|
|
4382
|
+
if nested:
|
|
4383
|
+
return nested
|
|
4384
|
+
event_type = value.get("type") or value.get("event_type") or value.get("kind")
|
|
4385
|
+
if event_type:
|
|
4386
|
+
payload = value.get("payload") if isinstance(value.get("payload"), dict) else value.get("data")
|
|
4387
|
+
if payload is not None:
|
|
4388
|
+
return f"{event_type}: {json.dumps(payload, ensure_ascii=False, separators=(',', ':'))}"
|
|
4389
|
+
return str(event_type)
|
|
4390
|
+
return None
|
|
4391
|
+
|
|
4392
|
+
|
|
4393
|
+
def _event_meta_from_sources(*sources: Any) -> dict[str, Any]:
|
|
4394
|
+
meta: dict[str, Any] = {}
|
|
4395
|
+
for source in sources:
|
|
4396
|
+
if not isinstance(source, dict):
|
|
4397
|
+
continue
|
|
4398
|
+
source_meta = source.get("meta")
|
|
4399
|
+
if isinstance(source_meta, dict):
|
|
4400
|
+
meta.update(source_meta)
|
|
4401
|
+
for key in (
|
|
4402
|
+
"room_id",
|
|
4403
|
+
"room",
|
|
4404
|
+
"channel",
|
|
4405
|
+
"thread_id",
|
|
4406
|
+
"parent_id",
|
|
4407
|
+
"message_id",
|
|
4408
|
+
"task_id",
|
|
4409
|
+
"round_id",
|
|
4410
|
+
"agent_id",
|
|
4411
|
+
"sender_id",
|
|
4412
|
+
"recipient_id",
|
|
4413
|
+
"type",
|
|
4414
|
+
"event_type",
|
|
4415
|
+
"kind",
|
|
4416
|
+
):
|
|
4417
|
+
value = source.get(key)
|
|
4418
|
+
if value is not None and key not in meta:
|
|
4419
|
+
meta[key] = value
|
|
4420
|
+
return meta
|
|
4421
|
+
|
|
4422
|
+
|
|
4350
4423
|
def _sse_payload_to_chat_payload(data_text: str, event_name: str, defaults: dict[str, Any]) -> dict[str, Any] | None:
|
|
4351
4424
|
text = (data_text or "").strip()
|
|
4352
4425
|
if not text or text == "[DONE]":
|
|
@@ -4372,28 +4445,24 @@ def _sse_payload_to_chat_payload(data_text: str, event_name: str, defaults: dict
|
|
|
4372
4445
|
method = str(parsed.get("method") or event_name or "message")
|
|
4373
4446
|
params = parsed.get("params") if isinstance(parsed.get("params"), dict) else {}
|
|
4374
4447
|
payload = parsed.get("payload") if isinstance(parsed.get("payload"), dict) else {}
|
|
4375
|
-
|
|
4376
|
-
if isinstance(
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
break
|
|
4384
|
-
if content != text:
|
|
4385
|
-
break
|
|
4448
|
+
data = params.get("data") if isinstance(params.get("data"), dict) else {}
|
|
4449
|
+
event = params.get("event") if isinstance(params.get("event"), dict) else {}
|
|
4450
|
+
nested_payload = (
|
|
4451
|
+
data.get("payload") if isinstance(data.get("payload"), dict) else
|
|
4452
|
+
event.get("payload") if isinstance(event.get("payload"), dict) else {}
|
|
4453
|
+
)
|
|
4454
|
+
meta.update(_event_meta_from_sources(parsed, params, payload, data, event, nested_payload))
|
|
4455
|
+
content = _event_payload_text(params) or _event_payload_text(payload) or _event_payload_text(data) or _event_payload_text(event) or content
|
|
4386
4456
|
kind = method.replace("notifications/claude/", "").replace("/", ".") if method else "sse"
|
|
4387
|
-
for key in ("room_id", "room", "thread_id", "parent_id", "message_id", "task_id", "round_id"):
|
|
4388
|
-
value = params.get(key, payload.get(key, parsed.get(key)))
|
|
4389
|
-
if value is not None:
|
|
4390
|
-
meta[key] = value
|
|
4391
4457
|
if allowed_events and method not in allowed_events and (event_name or "message") not in allowed_events:
|
|
4392
4458
|
return None
|
|
4459
|
+
channel = defaults.get("channel") or "default"
|
|
4460
|
+
if str(channel) == "default" and meta.get("channel"):
|
|
4461
|
+
channel = meta.get("channel")
|
|
4393
4462
|
return {
|
|
4394
|
-
"channel":
|
|
4395
|
-
"sender_id": defaults.get("sender_id") or "sse",
|
|
4396
|
-
"recipients": defaults.get("recipient") or defaults.get("recipients") or "all",
|
|
4463
|
+
"channel": channel,
|
|
4464
|
+
"sender_id": meta.get("sender_id") or meta.get("agent_id") or defaults.get("sender_id") or "sse",
|
|
4465
|
+
"recipients": meta.get("recipient_id") or defaults.get("recipient") or defaults.get("recipients") or "all",
|
|
4397
4466
|
"thread_id": meta.get("thread_id"),
|
|
4398
4467
|
"parent_id": meta.get("parent_id"),
|
|
4399
4468
|
"kind": kind,
|
|
@@ -4402,8 +4471,78 @@ def _sse_payload_to_chat_payload(data_text: str, event_name: str, defaults: dict
|
|
|
4402
4471
|
}
|
|
4403
4472
|
|
|
4404
4473
|
|
|
4474
|
+
def _channel_sse_set_state(name: str, **updates: Any) -> None:
|
|
4475
|
+
with _CHANNEL_SSE_LOCK:
|
|
4476
|
+
state = _CHANNEL_SSE_CONNECTIONS.get(name)
|
|
4477
|
+
if state:
|
|
4478
|
+
state.update(updates)
|
|
4479
|
+
|
|
4480
|
+
|
|
4481
|
+
def _channel_sse_absolute_endpoint(stream_url: str, endpoint: str) -> str:
|
|
4482
|
+
endpoint = (endpoint or "").strip()
|
|
4483
|
+
if endpoint.startswith(("http://", "https://")):
|
|
4484
|
+
return endpoint
|
|
4485
|
+
return urllib.parse.urljoin(stream_url, endpoint)
|
|
4486
|
+
|
|
4487
|
+
|
|
4488
|
+
def _mcp_sse_post_json(endpoint: str, headers: dict[str, str], payload: dict[str, Any], timeout: float) -> Any:
|
|
4489
|
+
request_headers = {**headers, "Content-Type": "application/json", "Accept": "application/json, text/event-stream"}
|
|
4490
|
+
req = urllib.request.Request(
|
|
4491
|
+
endpoint,
|
|
4492
|
+
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
|
4493
|
+
headers=request_headers,
|
|
4494
|
+
method="POST",
|
|
4495
|
+
)
|
|
4496
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
4497
|
+
data = response.read()
|
|
4498
|
+
if not data:
|
|
4499
|
+
return None
|
|
4500
|
+
try:
|
|
4501
|
+
return json.loads(data.decode("utf-8"))
|
|
4502
|
+
except Exception:
|
|
4503
|
+
return data.decode("utf-8", errors="replace")
|
|
4504
|
+
|
|
4505
|
+
|
|
4506
|
+
def _channel_sse_maybe_initialize_mcp(name: str, endpoint_text: str) -> None:
|
|
4507
|
+
with _CHANNEL_SSE_LOCK:
|
|
4508
|
+
state = _CHANNEL_SSE_CONNECTIONS.get(name)
|
|
4509
|
+
if not state:
|
|
4510
|
+
return
|
|
4511
|
+
if not bool(state.get("mcp_enabled", True)):
|
|
4512
|
+
return
|
|
4513
|
+
if state.get("mcp_initialized"):
|
|
4514
|
+
return
|
|
4515
|
+
stream_url = str(state.get("url") or "")
|
|
4516
|
+
headers = dict(state.get("headers") or {})
|
|
4517
|
+
timeout = max(5.0, min(120.0, float(state.get("mcp_timeout_seconds") or 20.0)))
|
|
4518
|
+
protocol_version = str(state.get("mcp_protocol_version") or "2024-11-05")
|
|
4519
|
+
endpoint = _channel_sse_absolute_endpoint(stream_url, endpoint_text)
|
|
4520
|
+
try:
|
|
4521
|
+
initialize = {
|
|
4522
|
+
"jsonrpc": "2.0",
|
|
4523
|
+
"id": 1,
|
|
4524
|
+
"method": "initialize",
|
|
4525
|
+
"params": {
|
|
4526
|
+
"protocolVersion": protocol_version,
|
|
4527
|
+
"capabilities": {},
|
|
4528
|
+
"clientInfo": {"name": "claude-any-channel-bridge", "version": VERSION},
|
|
4529
|
+
},
|
|
4530
|
+
}
|
|
4531
|
+
_mcp_sse_post_json(endpoint, headers, initialize, timeout)
|
|
4532
|
+
initialized = {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}
|
|
4533
|
+
_mcp_sse_post_json(endpoint, headers, initialized, timeout)
|
|
4534
|
+
_channel_sse_set_state(name, mcp_endpoint=endpoint, mcp_initialized=True, mcp_last_error=None)
|
|
4535
|
+
router_log("INFO", f"channel_sse_mcp_initialized name={name} endpoint={endpoint}")
|
|
4536
|
+
except Exception as exc:
|
|
4537
|
+
_channel_sse_set_state(name, mcp_endpoint=endpoint, mcp_initialized=False, mcp_last_error=f"{type(exc).__name__}: {exc}")
|
|
4538
|
+
router_log("WARN", f"channel_sse_mcp_initialize_failed name={name} endpoint={endpoint} error={type(exc).__name__}: {exc}")
|
|
4539
|
+
|
|
4540
|
+
|
|
4405
4541
|
def _channel_sse_dispatch(name: str, event_name: str, data_lines: list[str]) -> None:
|
|
4406
4542
|
data_text = "\n".join(data_lines)
|
|
4543
|
+
if (event_name or "").strip().lower() == "endpoint":
|
|
4544
|
+
_channel_sse_maybe_initialize_mcp(name, data_text)
|
|
4545
|
+
return
|
|
4407
4546
|
with _CHANNEL_SSE_LOCK:
|
|
4408
4547
|
state = _CHANNEL_SSE_CONNECTIONS.get(name)
|
|
4409
4548
|
if not state:
|
|
@@ -4511,6 +4650,12 @@ def start_channel_sse_connection(config: dict[str, Any]) -> dict[str, Any]:
|
|
|
4511
4650
|
"event_filter": event_filter,
|
|
4512
4651
|
"read_timeout_seconds": float(config.get("read_timeout_seconds") or config.get("timeout") or 300.0),
|
|
4513
4652
|
"retry_seconds": float(config.get("retry_seconds") or 5.0),
|
|
4653
|
+
"mcp_enabled": bool(config.get("mcp", config.get("mcp_enabled", True))),
|
|
4654
|
+
"mcp_endpoint": None,
|
|
4655
|
+
"mcp_initialized": False,
|
|
4656
|
+
"mcp_last_error": None,
|
|
4657
|
+
"mcp_protocol_version": str(config.get("mcp_protocol_version") or "2024-11-05"),
|
|
4658
|
+
"mcp_timeout_seconds": float(config.get("mcp_timeout_seconds") or 20.0),
|
|
4514
4659
|
}
|
|
4515
4660
|
_CHANNEL_SSE_CONNECTIONS[name] = state
|
|
4516
4661
|
thread = threading.Thread(target=_channel_sse_worker, args=(name,), daemon=True, name=f"claude-any-channel-sse-{name}")
|
|
@@ -4822,24 +4967,6 @@ def is_router_debug_request(body: dict[str, Any]) -> bool:
|
|
|
4822
4967
|
return "CLAUDE_ANY_ROUTER_DEBUG_ACCESS" in latest_user_text(body)
|
|
4823
4968
|
|
|
4824
4969
|
|
|
4825
|
-
def is_channel_bridge_request(body: dict[str, Any]) -> bool:
|
|
4826
|
-
return CHANNEL_BRIDGE_MARKER in latest_user_text(body)
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
def channel_bridge_args_from_body(body: dict[str, Any]) -> str:
|
|
4830
|
-
text = latest_user_text(body)
|
|
4831
|
-
if CHANNEL_BRIDGE_MARKER not in text:
|
|
4832
|
-
return "status"
|
|
4833
|
-
tail = text.split(CHANNEL_BRIDGE_MARKER, 1)[1]
|
|
4834
|
-
for line in tail.splitlines():
|
|
4835
|
-
stripped = line.strip()
|
|
4836
|
-
if not stripped:
|
|
4837
|
-
continue
|
|
4838
|
-
if stripped.lower().startswith("args:"):
|
|
4839
|
-
return stripped.split(":", 1)[1].strip() or "status"
|
|
4840
|
-
return tail.strip() or "status"
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
4970
|
def parse_channel_bridge_args(raw: str) -> tuple[str, dict[str, str]]:
|
|
4844
4971
|
text = (raw or "").strip()
|
|
4845
4972
|
if not text:
|
|
@@ -6433,96 +6560,6 @@ def maybe_handle_router_debug_request(handler: BaseHTTPRequestHandler, body: dic
|
|
|
6433
6560
|
return True
|
|
6434
6561
|
|
|
6435
6562
|
|
|
6436
|
-
def maybe_handle_channel_bridge_request(handler: BaseHTTPRequestHandler, body: dict[str, Any]) -> bool:
|
|
6437
|
-
if not is_channel_bridge_request(body):
|
|
6438
|
-
return False
|
|
6439
|
-
stream = bool(body.get("stream", True))
|
|
6440
|
-
raw_args = channel_bridge_args_from_body(body)
|
|
6441
|
-
command, options = parse_channel_bridge_args(raw_args)
|
|
6442
|
-
try:
|
|
6443
|
-
after = max(0, int(options.get("after") or "0"))
|
|
6444
|
-
except Exception:
|
|
6445
|
-
after = 0
|
|
6446
|
-
channel = options.get("channel") or None
|
|
6447
|
-
recipient = options.get("recipient") or options.get("recipient_id") or options.get("to") or None
|
|
6448
|
-
try:
|
|
6449
|
-
limit = max(1, min(100, int(options.get("limit") or "20")))
|
|
6450
|
-
except Exception:
|
|
6451
|
-
limit = 20
|
|
6452
|
-
model = str(body.get("model") or current_alias(load_config()))
|
|
6453
|
-
|
|
6454
|
-
if command == "status":
|
|
6455
|
-
latest = read_chat_messages(0, None, None, 1_000_000)
|
|
6456
|
-
last_id = int(latest[-1]["id"]) if latest else 0
|
|
6457
|
-
lines = [
|
|
6458
|
-
"Claude Any channel bridge is available.",
|
|
6459
|
-
"This is the router bridge API, separate from Claude Code's gated native --channels feature.",
|
|
6460
|
-
f"Last message id: {last_id}.",
|
|
6461
|
-
f"Poll: {ROUTER_BASE}/ca/channel/messages?after={last_id}&channel=default",
|
|
6462
|
-
f"Wait: {ROUTER_BASE}/ca/channel/wait?after={last_id}&timeout=60",
|
|
6463
|
-
f"SSE: {ROUTER_BASE}/ca/channel/stream?after={last_id}",
|
|
6464
|
-
f"Notify: POST {ROUTER_BASE}/ca/channel/notify with {{\"params\":{{\"content\":\"...\",\"meta\":{{}}}}}}",
|
|
6465
|
-
f"SSE connector status: {ROUTER_BASE}/ca/channel/sse/status",
|
|
6466
|
-
"SSE connector control: POST /ca/channel/sse/connect or /ca/channel/sse/disconnect.",
|
|
6467
|
-
"Slash usage: `/channel poll after=0`, `/channel wait after=0 timeout=60`, `/channel send channel=default to=all message=\"hello\"`, or `/channel sse`.",
|
|
6468
|
-
]
|
|
6469
|
-
write_anthropic_text_response(handler, model, "\n".join(lines), stream)
|
|
6470
|
-
return True
|
|
6471
|
-
|
|
6472
|
-
if command == "sse":
|
|
6473
|
-
statuses = channel_sse_status()
|
|
6474
|
-
if not statuses:
|
|
6475
|
-
text = "No channel SSE connectors are configured."
|
|
6476
|
-
else:
|
|
6477
|
-
lines = ["Channel SSE connectors:"]
|
|
6478
|
-
for name, state in statuses.items():
|
|
6479
|
-
running = "running" if state.get("running") else "stopped"
|
|
6480
|
-
received = int(state.get("messages_received") or 0)
|
|
6481
|
-
error = state.get("last_error") or ""
|
|
6482
|
-
suffix = f", last_error={error}" if error else ""
|
|
6483
|
-
lines.append(f"- {name}: {running}, received={received}, url={state.get('url')}{suffix}")
|
|
6484
|
-
text = "\n".join(lines)
|
|
6485
|
-
write_anthropic_text_response(handler, model, text, stream)
|
|
6486
|
-
return True
|
|
6487
|
-
|
|
6488
|
-
if command in {"poll", "wait"}:
|
|
6489
|
-
timeout = 0.0
|
|
6490
|
-
if command == "wait":
|
|
6491
|
-
try:
|
|
6492
|
-
timeout = max(0.0, min(300.0, float(options.get("timeout") or "60")))
|
|
6493
|
-
except Exception:
|
|
6494
|
-
timeout = 60.0
|
|
6495
|
-
deadline = time.time() + timeout
|
|
6496
|
-
messages = read_chat_messages(after, channel, recipient, limit)
|
|
6497
|
-
while not messages and timeout > 0 and time.time() < deadline:
|
|
6498
|
-
with _CHAT_CONDITION:
|
|
6499
|
-
_CHAT_CONDITION.wait(timeout=min(5.0, max(0.0, deadline - time.time())))
|
|
6500
|
-
messages = read_chat_messages(after, channel, recipient, limit)
|
|
6501
|
-
write_anthropic_text_response(handler, model, format_channel_messages(messages, after), stream)
|
|
6502
|
-
return True
|
|
6503
|
-
|
|
6504
|
-
if command in {"send", "post"}:
|
|
6505
|
-
message_text = options.get("message") or options.get("text") or ""
|
|
6506
|
-
if not message_text:
|
|
6507
|
-
write_anthropic_text_response(handler, model, "Channel send failed: provide message=\"...\".", stream)
|
|
6508
|
-
return True
|
|
6509
|
-
message = append_chat_message({
|
|
6510
|
-
"channel": channel or "default",
|
|
6511
|
-
"sender_id": options.get("sender") or options.get("sender_id") or "claude",
|
|
6512
|
-
"recipients": recipient or options.get("recipients") or "all",
|
|
6513
|
-
"thread_id": options.get("thread_id"),
|
|
6514
|
-
"parent_id": options.get("parent_id"),
|
|
6515
|
-
"kind": "message",
|
|
6516
|
-
"message": message_text,
|
|
6517
|
-
"meta": {"source": "slash_command"},
|
|
6518
|
-
})
|
|
6519
|
-
write_anthropic_text_response(handler, model, f"Channel message posted as id {message['id']}.", stream)
|
|
6520
|
-
return True
|
|
6521
|
-
|
|
6522
|
-
write_anthropic_text_response(handler, model, "Usage: `/channel status`, `/channel poll`, `/channel wait`, or `/channel send message=\"...\"`.", stream)
|
|
6523
|
-
return True
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
6563
|
def normalize_tool_arguments(tool_name: str, args: Any) -> dict[str, Any]:
|
|
6527
6564
|
if isinstance(args, dict):
|
|
6528
6565
|
return args
|
|
@@ -8151,9 +8188,6 @@ class RouterHandler(BaseHTTPRequestHandler):
|
|
|
8151
8188
|
if maybe_handle_router_debug_request(self, body):
|
|
8152
8189
|
EVENT_BUS.publish(level="info", category="router_debug.short_circuit", message="router debug request handled locally", request_id=request_id, provider=provider, model=str(body.get("model") or ""))
|
|
8153
8190
|
return
|
|
8154
|
-
if maybe_handle_channel_bridge_request(self, body):
|
|
8155
|
-
EVENT_BUS.publish(level="info", category="channel.short_circuit", message="channel bridge request handled locally", request_id=request_id, provider=provider, model=str(body.get("model") or ""))
|
|
8156
|
-
return
|
|
8157
8191
|
if maybe_handle_advisor_request(self, provider, pcfg, body):
|
|
8158
8192
|
EVENT_BUS.publish(level="info", category="advisor.short_circuit", message="advisor request handled locally", request_id=request_id, provider=provider, model=str(body.get("model") or ""))
|
|
8159
8193
|
return
|
|
@@ -8390,6 +8424,38 @@ def mask_secret(value: str | None) -> str:
|
|
|
8390
8424
|
return f"{text[:4]}...{text[-4:]}"
|
|
8391
8425
|
|
|
8392
8426
|
|
|
8427
|
+
SECRET_TEXT_PATTERNS = (
|
|
8428
|
+
re.compile(r"ak_key_[A-Za-z0-9_-]+_secret_[A-Za-z0-9_-]+"),
|
|
8429
|
+
re.compile(r"(AINET_API_KEY\s*=\s*)(\S+)", re.IGNORECASE),
|
|
8430
|
+
re.compile(r"(Authorization\s*:\s*Bearer\s+)(\S+)", re.IGNORECASE),
|
|
8431
|
+
re.compile(r"(token=)(ak_key_[A-Za-z0-9_-]+_secret_[A-Za-z0-9_-]+)", re.IGNORECASE),
|
|
8432
|
+
)
|
|
8433
|
+
|
|
8434
|
+
|
|
8435
|
+
def redact_sensitive_text(text: str) -> str:
|
|
8436
|
+
redacted = text
|
|
8437
|
+
redacted = SECRET_TEXT_PATTERNS[0].sub(lambda m: mask_secret(m.group(0)), redacted)
|
|
8438
|
+
for pattern in SECRET_TEXT_PATTERNS[1:]:
|
|
8439
|
+
redacted = pattern.sub(lambda m: f"{m.group(1)}{mask_secret(m.group(2))}", redacted)
|
|
8440
|
+
return redacted
|
|
8441
|
+
|
|
8442
|
+
|
|
8443
|
+
def redact_sensitive_obj(value: Any) -> Any:
|
|
8444
|
+
if isinstance(value, str):
|
|
8445
|
+
return redact_sensitive_text(value)
|
|
8446
|
+
if isinstance(value, list):
|
|
8447
|
+
return [redact_sensitive_obj(item) for item in value]
|
|
8448
|
+
if isinstance(value, dict):
|
|
8449
|
+
redacted: dict[str, Any] = {}
|
|
8450
|
+
for key, item in value.items():
|
|
8451
|
+
if str(key).lower() in {"api_key", "apikey", "token", "authorization", "bearer_token"}:
|
|
8452
|
+
redacted[key] = mask_secret(str(item))
|
|
8453
|
+
else:
|
|
8454
|
+
redacted[key] = redact_sensitive_obj(item)
|
|
8455
|
+
return redacted
|
|
8456
|
+
return value
|
|
8457
|
+
|
|
8458
|
+
|
|
8393
8459
|
def stored_api_key_mask(provider: str, pcfg: dict[str, Any]) -> str:
|
|
8394
8460
|
if provider == "nvidia-hosted":
|
|
8395
8461
|
return mask_secret(nvidia_api_key())
|
|
@@ -8579,6 +8645,7 @@ def status_lines() -> list[str]:
|
|
|
8579
8645
|
*([f"request_timeout_ms: {pcfg.get('request_timeout_ms', 'default')}"] if provider in ("vllm", "nvidia-hosted", "self-hosted-nim") else []),
|
|
8580
8646
|
*([f"stream_idle_timeout_ms: {pcfg.get('stream_idle_timeout_ms', 'auto')}"] if provider in ("vllm", "nvidia-hosted", "self-hosted-nim") else []),
|
|
8581
8647
|
f"claude_model: {current_upstream_model_id(provider, pcfg) if direct_native else current_alias(cfg)}",
|
|
8648
|
+
f"channels: {channel_status_text(cfg)}",
|
|
8582
8649
|
f"router: {'bypassed for native provider compatibility' if direct_native else (('up' if router_up() else 'down') + ' ' + ROUTER_BASE)}",
|
|
8583
8650
|
f"config: {CONFIG_PATH}",
|
|
8584
8651
|
]
|
|
@@ -8672,6 +8739,137 @@ def cmd_web_fetch(args: argparse.Namespace) -> None:
|
|
|
8672
8739
|
print(f"mcp_config: {WEB_TOOLS_MCP_CONFIG}")
|
|
8673
8740
|
|
|
8674
8741
|
|
|
8742
|
+
def channel_specs(cfg: dict[str, Any] | None = None) -> list[str]:
|
|
8743
|
+
cfg = cfg or load_config()
|
|
8744
|
+
raw = cfg.setdefault("claude_code", {}).get("channels", [])
|
|
8745
|
+
if isinstance(raw, str):
|
|
8746
|
+
items = [raw]
|
|
8747
|
+
elif isinstance(raw, list):
|
|
8748
|
+
items = raw
|
|
8749
|
+
else:
|
|
8750
|
+
items = []
|
|
8751
|
+
channels: list[str] = []
|
|
8752
|
+
seen: set[str] = set()
|
|
8753
|
+
for item in items:
|
|
8754
|
+
spec = str(item).strip()
|
|
8755
|
+
if not spec or spec in seen:
|
|
8756
|
+
continue
|
|
8757
|
+
seen.add(spec)
|
|
8758
|
+
channels.append(spec)
|
|
8759
|
+
return channels
|
|
8760
|
+
|
|
8761
|
+
|
|
8762
|
+
def channel_development_enabled(cfg: dict[str, Any] | None = None) -> bool:
|
|
8763
|
+
cfg = cfg or load_config()
|
|
8764
|
+
return bool(cfg.setdefault("claude_code", {}).get("development_channels", False))
|
|
8765
|
+
|
|
8766
|
+
|
|
8767
|
+
def channel_status_text(cfg: dict[str, Any] | None = None) -> str:
|
|
8768
|
+
cfg = cfg or load_config()
|
|
8769
|
+
channels = channel_specs(cfg)
|
|
8770
|
+
if not channels:
|
|
8771
|
+
return "off"
|
|
8772
|
+
suffix = "; dev" if channel_development_enabled(cfg) else ""
|
|
8773
|
+
return f"{len(channels)} channel{'s' if len(channels) != 1 else ''}{suffix}"
|
|
8774
|
+
|
|
8775
|
+
|
|
8776
|
+
def set_channel_development_enabled(enabled: bool) -> list[str]:
|
|
8777
|
+
cfg = load_config()
|
|
8778
|
+
cfg.setdefault("claude_code", {})["development_channels"] = bool(enabled)
|
|
8779
|
+
save_config(cfg)
|
|
8780
|
+
return [f"Development channels: {'on' if enabled else 'off'}."]
|
|
8781
|
+
|
|
8782
|
+
|
|
8783
|
+
def add_channel_spec(spec: str, *, development: bool = False) -> list[str]:
|
|
8784
|
+
spec = spec.strip()
|
|
8785
|
+
if not spec:
|
|
8786
|
+
return ["Channel spec was empty."]
|
|
8787
|
+
cfg = load_config()
|
|
8788
|
+
cc = cfg.setdefault("claude_code", {})
|
|
8789
|
+
channels = channel_specs(cfg)
|
|
8790
|
+
if spec not in channels:
|
|
8791
|
+
channels.append(spec)
|
|
8792
|
+
cc["channels"] = channels
|
|
8793
|
+
if development:
|
|
8794
|
+
cc["development_channels"] = True
|
|
8795
|
+
save_config(cfg)
|
|
8796
|
+
lines = [f"Channel added: {spec}."]
|
|
8797
|
+
if development:
|
|
8798
|
+
lines.append("Development channels: on.")
|
|
8799
|
+
return lines
|
|
8800
|
+
|
|
8801
|
+
|
|
8802
|
+
def remove_channel_spec(spec: str) -> list[str]:
|
|
8803
|
+
cfg = load_config()
|
|
8804
|
+
cc = cfg.setdefault("claude_code", {})
|
|
8805
|
+
before = channel_specs(cfg)
|
|
8806
|
+
after = [item for item in before if item != spec]
|
|
8807
|
+
cc["channels"] = after
|
|
8808
|
+
save_config(cfg)
|
|
8809
|
+
return [f"Channel removed: {spec}." if len(after) != len(before) else f"Channel was not configured: {spec}."]
|
|
8810
|
+
|
|
8811
|
+
|
|
8812
|
+
def clear_channel_specs() -> list[str]:
|
|
8813
|
+
cfg = load_config()
|
|
8814
|
+
cfg.setdefault("claude_code", {})["channels"] = []
|
|
8815
|
+
save_config(cfg)
|
|
8816
|
+
return ["Claude Code channels cleared."]
|
|
8817
|
+
|
|
8818
|
+
|
|
8819
|
+
def cmd_channels(args: argparse.Namespace) -> None:
|
|
8820
|
+
cfg = load_config()
|
|
8821
|
+
values = list(getattr(args, "values", []) or [])
|
|
8822
|
+
if not values:
|
|
8823
|
+
print(f"channels: {channel_status_text(cfg)}")
|
|
8824
|
+
for name, spec in OFFICIAL_CHANNEL_PLUGINS.items():
|
|
8825
|
+
mark = "*" if spec in channel_specs(cfg) else " "
|
|
8826
|
+
print(f" {mark} {name:<10} {spec}")
|
|
8827
|
+
for spec in channel_specs(cfg):
|
|
8828
|
+
if spec not in OFFICIAL_CHANNEL_PLUGINS.values():
|
|
8829
|
+
print(f" * custom {spec}")
|
|
8830
|
+
print(f"development_channels: {'on' if channel_development_enabled(cfg) else 'off'}")
|
|
8831
|
+
return
|
|
8832
|
+
head = values[0].strip().lower()
|
|
8833
|
+
if head in ("on", "enable", "add"):
|
|
8834
|
+
if len(values) < 2:
|
|
8835
|
+
raise SystemExit("Usage: claude-any channels add CHANNEL_SPEC")
|
|
8836
|
+
for line in add_channel_spec(values[1]):
|
|
8837
|
+
print(line)
|
|
8838
|
+
return
|
|
8839
|
+
if head in ("dev", "development"):
|
|
8840
|
+
if len(values) >= 2 and values[1].lower() in ("on", "off", "true", "false", "1", "0"):
|
|
8841
|
+
enabled = values[1].lower() in ("on", "true", "1")
|
|
8842
|
+
for line in set_channel_development_enabled(enabled):
|
|
8843
|
+
print(line)
|
|
8844
|
+
return
|
|
8845
|
+
if len(values) < 2:
|
|
8846
|
+
raise SystemExit("Usage: claude-any channels dev CHANNEL_SPEC | claude-any channels dev on|off")
|
|
8847
|
+
for line in add_channel_spec(values[1], development=True):
|
|
8848
|
+
print(line)
|
|
8849
|
+
return
|
|
8850
|
+
if head in ("off", "disable", "remove", "rm"):
|
|
8851
|
+
if len(values) < 2:
|
|
8852
|
+
raise SystemExit("Usage: claude-any channels remove CHANNEL_SPEC")
|
|
8853
|
+
for line in remove_channel_spec(values[1]):
|
|
8854
|
+
print(line)
|
|
8855
|
+
return
|
|
8856
|
+
if head in ("clear", "reset"):
|
|
8857
|
+
for line in clear_channel_specs():
|
|
8858
|
+
print(line)
|
|
8859
|
+
return
|
|
8860
|
+
if head in OFFICIAL_CHANNEL_PLUGINS:
|
|
8861
|
+
spec = OFFICIAL_CHANNEL_PLUGINS[head]
|
|
8862
|
+
if spec in channel_specs(cfg):
|
|
8863
|
+
for line in remove_channel_spec(spec):
|
|
8864
|
+
print(line)
|
|
8865
|
+
else:
|
|
8866
|
+
for line in add_channel_spec(spec):
|
|
8867
|
+
print(line)
|
|
8868
|
+
return
|
|
8869
|
+
for line in add_channel_spec(values[0]):
|
|
8870
|
+
print(line)
|
|
8871
|
+
|
|
8872
|
+
|
|
8675
8873
|
def cmd_ollama_native(args: argparse.Namespace) -> None:
|
|
8676
8874
|
cfg = load_config()
|
|
8677
8875
|
pcfg = cfg["providers"]["ollama"]
|
|
@@ -11858,8 +12056,9 @@ def main_menu_rows(cfg: dict[str, Any], provider: str, pcfg: dict[str, Any], lan
|
|
|
11858
12056
|
f"4. {ui_text('model', lang)} [{compact_text(pcfg.get('current_model', 'unset'), 62)}]",
|
|
11859
12057
|
f"5. {ui_text('advisor_model', lang)} [{compact_text(pcfg.get('advisor_model') or 'off', 62)}]",
|
|
11860
12058
|
f"6. {ui_text('options', lang)} [{compact_text(llm_options_status(provider, pcfg), 62)}]",
|
|
11861
|
-
f"7. {
|
|
11862
|
-
f"8. {ui_text('
|
|
12059
|
+
f"7. Channels [{channel_status_text(cfg)}]",
|
|
12060
|
+
f"8. {ui_text('test', lang)}",
|
|
12061
|
+
f"9. {ui_text('launch', lang)}",
|
|
11863
12062
|
ui_text("quit", lang),
|
|
11864
12063
|
]
|
|
11865
12064
|
|
|
@@ -11931,6 +12130,29 @@ def advisor_model_panel_rows(provider: str, pcfg: dict[str, Any]) -> tuple[list[
|
|
|
11931
12130
|
return rows, deduped_values
|
|
11932
12131
|
|
|
11933
12132
|
|
|
12133
|
+
def channel_panel_rows(cfg: dict[str, Any]) -> tuple[list[str], list[str]]:
|
|
12134
|
+
channels = channel_specs(cfg)
|
|
12135
|
+
dev_enabled = channel_development_enabled(cfg)
|
|
12136
|
+
rows: list[str] = []
|
|
12137
|
+
values: list[str] = []
|
|
12138
|
+
rows.append(f"Development channel loading [{'on' if dev_enabled else 'off'}]")
|
|
12139
|
+
values.append("__toggle_dev__")
|
|
12140
|
+
for name, spec in OFFICIAL_CHANNEL_PLUGINS.items():
|
|
12141
|
+
mark = "*" if spec in channels else " "
|
|
12142
|
+
rows.append(f"{mark} {name:<10} {spec}")
|
|
12143
|
+
values.append(spec)
|
|
12144
|
+
rows.append("+ Add development/custom channel...")
|
|
12145
|
+
values.append("__add_custom__")
|
|
12146
|
+
if channels:
|
|
12147
|
+
rows.append("- Remove channel...")
|
|
12148
|
+
values.append("__remove__")
|
|
12149
|
+
rows.append("Clear all channels")
|
|
12150
|
+
values.append("__clear__")
|
|
12151
|
+
rows.append("Back")
|
|
12152
|
+
values.append("back")
|
|
12153
|
+
return rows, values
|
|
12154
|
+
|
|
12155
|
+
|
|
11934
12156
|
def api_key_panel_rows(provider: str) -> tuple[list[str], list[str]]:
|
|
11935
12157
|
rows = [
|
|
11936
12158
|
"Type or paste API key as hidden input",
|
|
@@ -12231,7 +12453,7 @@ def portable_language_menu() -> int:
|
|
|
12231
12453
|
|
|
12232
12454
|
def portable_prelaunch_menu() -> int:
|
|
12233
12455
|
enable_ansi()
|
|
12234
|
-
main_idx =
|
|
12456
|
+
main_idx = 9 if settings_ready_except_api_key() else 0
|
|
12235
12457
|
panel: str | None = None
|
|
12236
12458
|
panel_idx = 0
|
|
12237
12459
|
panel_rows: list[str] = []
|
|
@@ -12273,6 +12495,8 @@ def portable_prelaunch_menu() -> int:
|
|
|
12273
12495
|
panel_rows, panel_values = ["Run compatibility test", "Back"], ["run", "back"]
|
|
12274
12496
|
elif name == "options":
|
|
12275
12497
|
panel_rows, panel_values = llm_option_panel_rows(provider, pcfg, cfg.get("language", "en"))
|
|
12498
|
+
elif name == "channels":
|
|
12499
|
+
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
12276
12500
|
elif name == "context":
|
|
12277
12501
|
panel_rows, panel_values = context_setup_panel_rows(provider, pcfg, cfg.get("language", "en"))
|
|
12278
12502
|
elif name == "preset":
|
|
@@ -12449,7 +12673,40 @@ def portable_prelaunch_menu() -> int:
|
|
|
12449
12673
|
messages = lines[-8:] if lines else ["Test produced no output."]
|
|
12450
12674
|
panel_rows, panel_values = ["Run compatibility test again", "Back"], ["run", "back"]
|
|
12451
12675
|
refresh_checks()
|
|
12452
|
-
main_idx =
|
|
12676
|
+
main_idx = 9 if "Compatibility: OK" in out else 4
|
|
12677
|
+
elif panel == "channels":
|
|
12678
|
+
if value == "back":
|
|
12679
|
+
close_panel()
|
|
12680
|
+
elif value == "__toggle_dev__":
|
|
12681
|
+
messages = set_channel_development_enabled(not channel_development_enabled(cfg))
|
|
12682
|
+
cfg = load_config()
|
|
12683
|
+
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
12684
|
+
panel_idx = 0
|
|
12685
|
+
elif value == "__add_custom__":
|
|
12686
|
+
spec = prompt_menu_value("Channel spec (for example plugin:ainet@local or server:ainet)", restore_tty=restore_line_mode, raw_tty=restore_raw_mode)
|
|
12687
|
+
if spec:
|
|
12688
|
+
messages = add_channel_spec(spec, development=True)
|
|
12689
|
+
cfg = load_config()
|
|
12690
|
+
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
12691
|
+
elif value == "__remove__":
|
|
12692
|
+
spec = prompt_menu_value("Channel spec to remove", "", restore_tty=restore_line_mode, raw_tty=restore_raw_mode)
|
|
12693
|
+
if spec:
|
|
12694
|
+
messages = remove_channel_spec(spec)
|
|
12695
|
+
cfg = load_config()
|
|
12696
|
+
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
12697
|
+
elif value == "__clear__":
|
|
12698
|
+
messages = clear_channel_specs()
|
|
12699
|
+
cfg = load_config()
|
|
12700
|
+
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
12701
|
+
panel_idx = 0
|
|
12702
|
+
elif value:
|
|
12703
|
+
if value in channel_specs(cfg):
|
|
12704
|
+
messages = remove_channel_spec(value)
|
|
12705
|
+
else:
|
|
12706
|
+
messages = add_channel_spec(value)
|
|
12707
|
+
cfg = load_config()
|
|
12708
|
+
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
12709
|
+
refresh_checks()
|
|
12453
12710
|
elif panel == "options":
|
|
12454
12711
|
if value == "back":
|
|
12455
12712
|
close_panel()
|
|
@@ -12548,13 +12805,13 @@ def portable_prelaunch_menu() -> int:
|
|
|
12548
12805
|
continue
|
|
12549
12806
|
|
|
12550
12807
|
if key in ("up", "k"):
|
|
12551
|
-
main_idx = (main_idx - 1) %
|
|
12808
|
+
main_idx = (main_idx - 1) % 11
|
|
12552
12809
|
elif key in ("down", "j"):
|
|
12553
|
-
main_idx = (main_idx + 1) %
|
|
12810
|
+
main_idx = (main_idx + 1) % 11
|
|
12554
12811
|
elif key in ("esc", "q"):
|
|
12555
12812
|
return 10
|
|
12556
12813
|
elif key == "enter":
|
|
12557
|
-
actions = ["language", "provider", "api-key", "base-url", "model", "advisor-model", "options", "test", "launch", "quit"]
|
|
12814
|
+
actions = ["language", "provider", "api-key", "base-url", "model", "advisor-model", "options", "channels", "test", "launch", "quit"]
|
|
12558
12815
|
action = actions[main_idx]
|
|
12559
12816
|
if action == "launch":
|
|
12560
12817
|
blockers = launch_readiness_errors()
|
|
@@ -12643,6 +12900,18 @@ def has_passthrough_option(passthrough: list[str], *names: str) -> bool:
|
|
|
12643
12900
|
return any(arg in names or any(arg.startswith(name + "=") for name in names) for arg in passthrough)
|
|
12644
12901
|
|
|
12645
12902
|
|
|
12903
|
+
def claude_channel_args(cfg: dict[str, Any], passthrough: list[str]) -> list[str]:
|
|
12904
|
+
channels = channel_specs(cfg)
|
|
12905
|
+
if not channels or has_passthrough_option(passthrough, "--channels"):
|
|
12906
|
+
return []
|
|
12907
|
+
args: list[str] = []
|
|
12908
|
+
if channel_development_enabled(cfg) and not has_passthrough_option(passthrough, "--dangerously-load-development-channels"):
|
|
12909
|
+
args.append("--dangerously-load-development-channels")
|
|
12910
|
+
args.append("--channels")
|
|
12911
|
+
args.extend(channels)
|
|
12912
|
+
return args
|
|
12913
|
+
|
|
12914
|
+
|
|
12646
12915
|
def write_web_tools_mcp_config(cfg: dict[str, Any]) -> Path:
|
|
12647
12916
|
web = cfg.get("web_search", {})
|
|
12648
12917
|
package = web.get("package") or "ddg-mcp-search"
|
|
@@ -12982,6 +13251,7 @@ def launch_claude(
|
|
|
12982
13251
|
extra_args.extend(["--mcp-config", str(write_duckduckgo_mcp_config(cfg))])
|
|
12983
13252
|
if should_append_compat_prompt(provider, cfg) and not has_passthrough_option(passthrough, "--system-prompt"):
|
|
12984
13253
|
extra_args.extend(["--append-system-prompt", NON_ANTHROPIC_COMPAT_PROMPT])
|
|
13254
|
+
extra_args.extend(claude_channel_args(cfg, passthrough))
|
|
12985
13255
|
cmd = [
|
|
12986
13256
|
claude,
|
|
12987
13257
|
"--dangerously-skip-permissions",
|
|
@@ -13012,6 +13282,7 @@ Control plane, runs before Claude Code and does not require LLM connectivity:
|
|
|
13012
13282
|
claude-any set-api-key PROVIDER KEY
|
|
13013
13283
|
claude-any web-search [on|off] Auto-attach DuckDuckGo MCP for non-native providers
|
|
13014
13284
|
claude-any web-fetch [on|off] Auto-attach fetch MCP for web page content
|
|
13285
|
+
claude-any channels [cmd] Configure Claude Code --channels auto-injection
|
|
13015
13286
|
claude-any ollama-native [on|off] Use Ollama's official Claude Code env path
|
|
13016
13287
|
claude-any ollama-options [provider] [key=value ...]
|
|
13017
13288
|
Set Ollama num_ctx/options/keep_alive/think
|
|
@@ -13050,6 +13321,11 @@ Headless setup flags, namespaced to avoid Claude CLI collisions:
|
|
|
13050
13321
|
claude-any --ca-no-web-search Disable DuckDuckGo MCP for this launch
|
|
13051
13322
|
claude-any --ca-web-fetch Enable fetch MCP
|
|
13052
13323
|
claude-any --ca-no-web-fetch Disable fetch MCP
|
|
13324
|
+
claude-any --ca-channel SPEC Add an official/approved Claude Code channel
|
|
13325
|
+
claude-any --ca-dev-channel SPEC Add a development channel and enable dev loading
|
|
13326
|
+
claude-any --ca-development-channels on|off
|
|
13327
|
+
Auto-add --dangerously-load-development-channels
|
|
13328
|
+
claude-any --ca-clear-channels Clear saved channel auto-injection specs
|
|
13053
13329
|
claude-any --ca-no-self-update-check
|
|
13054
13330
|
Skip Claude Any npm self-update check
|
|
13055
13331
|
claude-any --ca-no-update-check Skip Claude Code update check for this launch
|
|
@@ -13157,6 +13433,28 @@ def apply_headless_env_config() -> tuple[bool, bool | None, bool | None, bool |
|
|
|
13157
13433
|
if ollama_values:
|
|
13158
13434
|
cmd_ollama_options(argparse.Namespace(values=ollama_values))
|
|
13159
13435
|
skip_menu = True
|
|
13436
|
+
channel_values = [
|
|
13437
|
+
item.strip()
|
|
13438
|
+
for item in re.split(r"[\s,]+", os.environ.get("CLAUDE_ANY_CHANNELS", "").strip())
|
|
13439
|
+
if item.strip()
|
|
13440
|
+
]
|
|
13441
|
+
for channel_value in channel_values:
|
|
13442
|
+
add_channel_spec(channel_value)
|
|
13443
|
+
skip_menu = True
|
|
13444
|
+
dev_channel_values = [
|
|
13445
|
+
item.strip()
|
|
13446
|
+
for item in re.split(r"[\s,]+", os.environ.get("CLAUDE_ANY_DEV_CHANNELS", "").strip())
|
|
13447
|
+
if item.strip()
|
|
13448
|
+
]
|
|
13449
|
+
for channel_value in dev_channel_values:
|
|
13450
|
+
add_channel_spec(channel_value, development=True)
|
|
13451
|
+
skip_menu = True
|
|
13452
|
+
dev_channels = os.environ.get("CLAUDE_ANY_DEVELOPMENT_CHANNELS", "").strip().lower()
|
|
13453
|
+
if dev_channels:
|
|
13454
|
+
if dev_channels not in ("on", "off", "true", "false", "1", "0"):
|
|
13455
|
+
raise SystemExit("CLAUDE_ANY_DEVELOPMENT_CHANNELS must be on or off")
|
|
13456
|
+
set_channel_development_enabled(dev_channels in ("on", "true", "1"))
|
|
13457
|
+
skip_menu = True
|
|
13160
13458
|
return skip_menu, web_search_override, update_check_override, self_update_check_override, force_menu
|
|
13161
13459
|
|
|
13162
13460
|
|
|
@@ -13216,6 +13514,9 @@ def run_cli(argv: list[str]) -> int:
|
|
|
13216
13514
|
if head in ("web-fetch", "webfetch"):
|
|
13217
13515
|
cmd_web_fetch(argparse.Namespace(value=rest[0] if rest else None))
|
|
13218
13516
|
return 0
|
|
13517
|
+
if head in ("channels", "channel"):
|
|
13518
|
+
cmd_channels(argparse.Namespace(values=rest))
|
|
13519
|
+
return 0
|
|
13219
13520
|
if head in ("ollama-native", "ollama-compat"):
|
|
13220
13521
|
cmd_ollama_native(argparse.Namespace(value=rest[0] if rest else None))
|
|
13221
13522
|
return 0
|
|
@@ -13550,6 +13851,48 @@ def run_cli(argv: list[str]) -> int:
|
|
|
13550
13851
|
cmd_web_fetch(argparse.Namespace(value="off"))
|
|
13551
13852
|
skip_menu = True
|
|
13552
13853
|
i += 1
|
|
13854
|
+
elif arg == "--ca-channel" or arg.startswith("--ca-channel="):
|
|
13855
|
+
value = arg.split("=", 1)[1] if "=" in arg else None
|
|
13856
|
+
if value is None:
|
|
13857
|
+
if i + 1 >= len(argv):
|
|
13858
|
+
raise SystemExit("Missing channel spec for --ca-channel")
|
|
13859
|
+
value = argv[i + 1]
|
|
13860
|
+
i += 2
|
|
13861
|
+
else:
|
|
13862
|
+
i += 1
|
|
13863
|
+
for line in add_channel_spec(value):
|
|
13864
|
+
print(line)
|
|
13865
|
+
skip_menu = True
|
|
13866
|
+
elif arg == "--ca-dev-channel" or arg.startswith("--ca-dev-channel="):
|
|
13867
|
+
value = arg.split("=", 1)[1] if "=" in arg else None
|
|
13868
|
+
if value is None:
|
|
13869
|
+
if i + 1 >= len(argv):
|
|
13870
|
+
raise SystemExit("Missing channel spec for --ca-dev-channel")
|
|
13871
|
+
value = argv[i + 1]
|
|
13872
|
+
i += 2
|
|
13873
|
+
else:
|
|
13874
|
+
i += 1
|
|
13875
|
+
for line in add_channel_spec(value, development=True):
|
|
13876
|
+
print(line)
|
|
13877
|
+
skip_menu = True
|
|
13878
|
+
elif arg == "--ca-development-channels" or arg.startswith("--ca-development-channels="):
|
|
13879
|
+
value = arg.split("=", 1)[1] if "=" in arg else None
|
|
13880
|
+
if value is None:
|
|
13881
|
+
if i + 1 >= len(argv):
|
|
13882
|
+
raise SystemExit("Missing on/off for --ca-development-channels")
|
|
13883
|
+
value = argv[i + 1]
|
|
13884
|
+
i += 2
|
|
13885
|
+
else:
|
|
13886
|
+
i += 1
|
|
13887
|
+
enabled = value.strip().lower() in ("on", "enable", "enabled", "true", "1")
|
|
13888
|
+
for line in set_channel_development_enabled(enabled):
|
|
13889
|
+
print(line)
|
|
13890
|
+
skip_menu = True
|
|
13891
|
+
elif arg == "--ca-clear-channels":
|
|
13892
|
+
for line in clear_channel_specs():
|
|
13893
|
+
print(line)
|
|
13894
|
+
skip_menu = True
|
|
13895
|
+
i += 1
|
|
13553
13896
|
elif arg == "--ca-no-update-check":
|
|
13554
13897
|
update_check = False
|
|
13555
13898
|
skip_menu = True
|
|
@@ -13620,6 +13963,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
13620
13963
|
wf = sub.add_parser("web-fetch")
|
|
13621
13964
|
wf.add_argument("value", nargs="?")
|
|
13622
13965
|
wf.set_defaults(func=cmd_web_fetch)
|
|
13966
|
+
ch = sub.add_parser("channels")
|
|
13967
|
+
ch.add_argument("values", nargs="*")
|
|
13968
|
+
ch.set_defaults(func=cmd_channels)
|
|
13623
13969
|
on = sub.add_parser("ollama-native")
|
|
13624
13970
|
on.add_argument("value", nargs="?")
|
|
13625
13971
|
on.set_defaults(func=cmd_ollama_native)
|
package/docs/README.ja.md
CHANGED
|
@@ -61,7 +61,7 @@ vLLM、NVIDIA hosted、self-hosted NIM を選択し、通常の Claude Code 引
|
|
|
61
61
|
|
|
62
62
|
Credits: One Ciel LLC
|
|
63
63
|
|
|
64
|
-
現在のバージョン: `0.1.
|
|
64
|
+
現在のバージョン: `0.1.71`
|
|
65
65
|
|
|
66
66
|
## 作られた理由
|
|
67
67
|
|
|
@@ -365,6 +365,14 @@ Windows/Linux 管理、クリーンアップスクリプト、定期的なセキ
|
|
|
365
365
|
|
|
366
366
|
## 変更履歴
|
|
367
367
|
|
|
368
|
+
### 0.1.71
|
|
369
|
+
|
|
370
|
+
- **MCP SSE channel 初期化**: channel bridge は MCP `endpoint` event を受けると
|
|
371
|
+
`initialize` と `notifications/initialized` を自動送信し、AI-Net 型 push
|
|
372
|
+
notification が Claude Any に流れ始めるようになりました。
|
|
373
|
+
- **Channel SSE 診断情報**: connector status に MCP endpoint、初期化状態、
|
|
374
|
+
最後の MCP 初期化エラーを表示します。
|
|
375
|
+
|
|
368
376
|
### 0.1.70
|
|
369
377
|
|
|
370
378
|
- **Linux menu debug log 修正**: key-debug log は global `/tmp` ではなく
|
package/docs/README.ko.md
CHANGED
|
@@ -67,7 +67,7 @@ NVIDIA hosted, self-hosted NIM을 선택하고, Claude Code의 일반 인자는
|
|
|
67
67
|
|
|
68
68
|
Credits: One Ciel LLC
|
|
69
69
|
|
|
70
|
-
현재 버전: `0.1.
|
|
70
|
+
현재 버전: `0.1.71`
|
|
71
71
|
|
|
72
72
|
## 왜 만들었나
|
|
73
73
|
|
|
@@ -371,6 +371,14 @@ Windows 이벤트 로그 리뷰, 바이러스/랜섬웨어 침입 시도 정리,
|
|
|
371
371
|
|
|
372
372
|
## 변경 이력
|
|
373
373
|
|
|
374
|
+
### 0.1.71
|
|
375
|
+
|
|
376
|
+
- **MCP SSE channel 초기화**: channel bridge가 MCP `endpoint` 이벤트를 받으면
|
|
377
|
+
`initialize`와 `notifications/initialized`를 자동 전송하므로, AI-Net 스타일
|
|
378
|
+
push notification이 Claude Any로 흐를 수 있습니다.
|
|
379
|
+
- **Channel SSE 진단 정보**: connector 상태에 MCP endpoint, 초기화 여부, 마지막
|
|
380
|
+
MCP 초기화 오류를 표시합니다.
|
|
381
|
+
|
|
374
382
|
### 0.1.70
|
|
375
383
|
|
|
376
384
|
- **Linux 메뉴 디버그 로그 수정**: key-debug 로그를 전역 `/tmp`가 아니라 사용자
|
package/docs/README.zh.md
CHANGED
|
@@ -61,7 +61,7 @@ NIM,并把普通 Claude Code 参数原样传递。
|
|
|
61
61
|
|
|
62
62
|
Credits: One Ciel LLC
|
|
63
63
|
|
|
64
|
-
当前版本: `0.1.
|
|
64
|
+
当前版本: `0.1.71`
|
|
65
65
|
|
|
66
66
|
## 为什么存在
|
|
67
67
|
|
|
@@ -351,6 +351,14 @@ Hermes 格式模型或部分较旧的 Qwen tool template。
|
|
|
351
351
|
|
|
352
352
|
## 更新日志
|
|
353
353
|
|
|
354
|
+
### 0.1.71
|
|
355
|
+
|
|
356
|
+
- **MCP SSE channel 初始化**:channel bridge 收到 MCP `endpoint` 事件后会自动发送
|
|
357
|
+
`initialize` 与 `notifications/initialized`,让 AI-Net 风格的 push
|
|
358
|
+
notification 可以流入 Claude Any。
|
|
359
|
+
- **Channel SSE 诊断信息**:connector status 现在会显示 MCP endpoint、初始化状态
|
|
360
|
+
以及最后一次 MCP 初始化错误。
|
|
361
|
+
|
|
354
362
|
### 0.1.70
|
|
355
363
|
|
|
356
364
|
- **Linux 菜单调试日志修复**:key-debug 日志现在写入用户的 Claude Any config
|
package/docs/manual.md
CHANGED
package/package.json
CHANGED