@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 CHANGED
@@ -68,7 +68,7 @@ arguments through unchanged.
68
68
 
69
69
  Credits: One Ciel LLC
70
70
 
71
- Current version: `0.1.70`
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.70"
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 and item.get("channel") != channel:
4290
- continue
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
- meta.update({k: v for k, v in (params.get("meta") if isinstance(params.get("meta"), dict) else {}).items()})
4376
- if isinstance(parsed.get("meta"), dict):
4377
- meta.update(parsed["meta"])
4378
- for source in (params, payload, parsed):
4379
- for key in ("content", "message", "text", "body"):
4380
- value = source.get(key)
4381
- if value is not None:
4382
- content = str(value)
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": defaults.get("channel") or "default",
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. {ui_text('test', lang)}",
11862
- f"8. {ui_text('launch', lang)}",
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 = 7 if settings_ready_except_api_key() else 0
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 = 8 if "Compatibility: OK" in out else 4
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) % 10
12808
+ main_idx = (main_idx - 1) % 11
12552
12809
  elif key in ("down", "j"):
12553
- main_idx = (main_idx + 1) % 10
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.70`
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`
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.70`
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
@@ -10,7 +10,7 @@ Code starts, while passing normal Claude Code arguments through unchanged.
10
10
 
11
11
  Credits: One Ciel LLC
12
12
 
13
- Current version: `0.1.70`
13
+ Current version: `0.1.71`
14
14
 
15
15
  ## Install
16
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneciel-ai/claude-any",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
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",