@oneciel-ai/claude-any 0.1.71 → 0.1.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude_any.py +431 -161
- package/package.json +1 -1
package/claude_any.py
CHANGED
|
@@ -95,6 +95,12 @@ 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
105
|
VERSION = "0.1.71"
|
|
100
106
|
CREDITS = "Credits: One Ciel LLC"
|
|
@@ -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)
|
|
@@ -4350,6 +4352,74 @@ def channel_sse_status() -> dict[str, Any]:
|
|
|
4350
4352
|
return {name: _channel_sse_status_public(name, state) for name, state in _CHANNEL_SSE_CONNECTIONS.items()}
|
|
4351
4353
|
|
|
4352
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
|
+
|
|
4353
4423
|
def _sse_payload_to_chat_payload(data_text: str, event_name: str, defaults: dict[str, Any]) -> dict[str, Any] | None:
|
|
4354
4424
|
text = (data_text or "").strip()
|
|
4355
4425
|
if not text or text == "[DONE]":
|
|
@@ -4375,28 +4445,24 @@ def _sse_payload_to_chat_payload(data_text: str, event_name: str, defaults: dict
|
|
|
4375
4445
|
method = str(parsed.get("method") or event_name or "message")
|
|
4376
4446
|
params = parsed.get("params") if isinstance(parsed.get("params"), dict) else {}
|
|
4377
4447
|
payload = parsed.get("payload") if isinstance(parsed.get("payload"), dict) else {}
|
|
4378
|
-
|
|
4379
|
-
if isinstance(
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
break
|
|
4387
|
-
if content != text:
|
|
4388
|
-
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
|
|
4389
4456
|
kind = method.replace("notifications/claude/", "").replace("/", ".") if method else "sse"
|
|
4390
|
-
for key in ("room_id", "room", "thread_id", "parent_id", "message_id", "task_id", "round_id"):
|
|
4391
|
-
value = params.get(key, payload.get(key, parsed.get(key)))
|
|
4392
|
-
if value is not None:
|
|
4393
|
-
meta[key] = value
|
|
4394
4457
|
if allowed_events and method not in allowed_events and (event_name or "message") not in allowed_events:
|
|
4395
4458
|
return None
|
|
4459
|
+
channel = defaults.get("channel") or "default"
|
|
4460
|
+
if str(channel) == "default" and meta.get("channel"):
|
|
4461
|
+
channel = meta.get("channel")
|
|
4396
4462
|
return {
|
|
4397
|
-
"channel":
|
|
4398
|
-
"sender_id": defaults.get("sender_id") or "sse",
|
|
4399
|
-
"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",
|
|
4400
4466
|
"thread_id": meta.get("thread_id"),
|
|
4401
4467
|
"parent_id": meta.get("parent_id"),
|
|
4402
4468
|
"kind": kind,
|
|
@@ -4901,24 +4967,6 @@ def is_router_debug_request(body: dict[str, Any]) -> bool:
|
|
|
4901
4967
|
return "CLAUDE_ANY_ROUTER_DEBUG_ACCESS" in latest_user_text(body)
|
|
4902
4968
|
|
|
4903
4969
|
|
|
4904
|
-
def is_channel_bridge_request(body: dict[str, Any]) -> bool:
|
|
4905
|
-
return CHANNEL_BRIDGE_MARKER in latest_user_text(body)
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
def channel_bridge_args_from_body(body: dict[str, Any]) -> str:
|
|
4909
|
-
text = latest_user_text(body)
|
|
4910
|
-
if CHANNEL_BRIDGE_MARKER not in text:
|
|
4911
|
-
return "status"
|
|
4912
|
-
tail = text.split(CHANNEL_BRIDGE_MARKER, 1)[1]
|
|
4913
|
-
for line in tail.splitlines():
|
|
4914
|
-
stripped = line.strip()
|
|
4915
|
-
if not stripped:
|
|
4916
|
-
continue
|
|
4917
|
-
if stripped.lower().startswith("args:"):
|
|
4918
|
-
return stripped.split(":", 1)[1].strip() or "status"
|
|
4919
|
-
return tail.strip() or "status"
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
4970
|
def parse_channel_bridge_args(raw: str) -> tuple[str, dict[str, str]]:
|
|
4923
4971
|
text = (raw or "").strip()
|
|
4924
4972
|
if not text:
|
|
@@ -6512,96 +6560,6 @@ def maybe_handle_router_debug_request(handler: BaseHTTPRequestHandler, body: dic
|
|
|
6512
6560
|
return True
|
|
6513
6561
|
|
|
6514
6562
|
|
|
6515
|
-
def maybe_handle_channel_bridge_request(handler: BaseHTTPRequestHandler, body: dict[str, Any]) -> bool:
|
|
6516
|
-
if not is_channel_bridge_request(body):
|
|
6517
|
-
return False
|
|
6518
|
-
stream = bool(body.get("stream", True))
|
|
6519
|
-
raw_args = channel_bridge_args_from_body(body)
|
|
6520
|
-
command, options = parse_channel_bridge_args(raw_args)
|
|
6521
|
-
try:
|
|
6522
|
-
after = max(0, int(options.get("after") or "0"))
|
|
6523
|
-
except Exception:
|
|
6524
|
-
after = 0
|
|
6525
|
-
channel = options.get("channel") or None
|
|
6526
|
-
recipient = options.get("recipient") or options.get("recipient_id") or options.get("to") or None
|
|
6527
|
-
try:
|
|
6528
|
-
limit = max(1, min(100, int(options.get("limit") or "20")))
|
|
6529
|
-
except Exception:
|
|
6530
|
-
limit = 20
|
|
6531
|
-
model = str(body.get("model") or current_alias(load_config()))
|
|
6532
|
-
|
|
6533
|
-
if command == "status":
|
|
6534
|
-
latest = read_chat_messages(0, None, None, 1_000_000)
|
|
6535
|
-
last_id = int(latest[-1]["id"]) if latest else 0
|
|
6536
|
-
lines = [
|
|
6537
|
-
"Claude Any channel bridge is available.",
|
|
6538
|
-
"This is the router bridge API, separate from Claude Code's gated native --channels feature.",
|
|
6539
|
-
f"Last message id: {last_id}.",
|
|
6540
|
-
f"Poll: {ROUTER_BASE}/ca/channel/messages?after={last_id}&channel=default",
|
|
6541
|
-
f"Wait: {ROUTER_BASE}/ca/channel/wait?after={last_id}&timeout=60",
|
|
6542
|
-
f"SSE: {ROUTER_BASE}/ca/channel/stream?after={last_id}",
|
|
6543
|
-
f"Notify: POST {ROUTER_BASE}/ca/channel/notify with {{\"params\":{{\"content\":\"...\",\"meta\":{{}}}}}}",
|
|
6544
|
-
f"SSE connector status: {ROUTER_BASE}/ca/channel/sse/status",
|
|
6545
|
-
"SSE connector control: POST /ca/channel/sse/connect or /ca/channel/sse/disconnect.",
|
|
6546
|
-
"Slash usage: `/channel poll after=0`, `/channel wait after=0 timeout=60`, `/channel send channel=default to=all message=\"hello\"`, or `/channel sse`.",
|
|
6547
|
-
]
|
|
6548
|
-
write_anthropic_text_response(handler, model, "\n".join(lines), stream)
|
|
6549
|
-
return True
|
|
6550
|
-
|
|
6551
|
-
if command == "sse":
|
|
6552
|
-
statuses = channel_sse_status()
|
|
6553
|
-
if not statuses:
|
|
6554
|
-
text = "No channel SSE connectors are configured."
|
|
6555
|
-
else:
|
|
6556
|
-
lines = ["Channel SSE connectors:"]
|
|
6557
|
-
for name, state in statuses.items():
|
|
6558
|
-
running = "running" if state.get("running") else "stopped"
|
|
6559
|
-
received = int(state.get("messages_received") or 0)
|
|
6560
|
-
error = state.get("last_error") or ""
|
|
6561
|
-
suffix = f", last_error={error}" if error else ""
|
|
6562
|
-
lines.append(f"- {name}: {running}, received={received}, url={state.get('url')}{suffix}")
|
|
6563
|
-
text = "\n".join(lines)
|
|
6564
|
-
write_anthropic_text_response(handler, model, text, stream)
|
|
6565
|
-
return True
|
|
6566
|
-
|
|
6567
|
-
if command in {"poll", "wait"}:
|
|
6568
|
-
timeout = 0.0
|
|
6569
|
-
if command == "wait":
|
|
6570
|
-
try:
|
|
6571
|
-
timeout = max(0.0, min(300.0, float(options.get("timeout") or "60")))
|
|
6572
|
-
except Exception:
|
|
6573
|
-
timeout = 60.0
|
|
6574
|
-
deadline = time.time() + timeout
|
|
6575
|
-
messages = read_chat_messages(after, channel, recipient, limit)
|
|
6576
|
-
while not messages and timeout > 0 and time.time() < deadline:
|
|
6577
|
-
with _CHAT_CONDITION:
|
|
6578
|
-
_CHAT_CONDITION.wait(timeout=min(5.0, max(0.0, deadline - time.time())))
|
|
6579
|
-
messages = read_chat_messages(after, channel, recipient, limit)
|
|
6580
|
-
write_anthropic_text_response(handler, model, format_channel_messages(messages, after), stream)
|
|
6581
|
-
return True
|
|
6582
|
-
|
|
6583
|
-
if command in {"send", "post"}:
|
|
6584
|
-
message_text = options.get("message") or options.get("text") or ""
|
|
6585
|
-
if not message_text:
|
|
6586
|
-
write_anthropic_text_response(handler, model, "Channel send failed: provide message=\"...\".", stream)
|
|
6587
|
-
return True
|
|
6588
|
-
message = append_chat_message({
|
|
6589
|
-
"channel": channel or "default",
|
|
6590
|
-
"sender_id": options.get("sender") or options.get("sender_id") or "claude",
|
|
6591
|
-
"recipients": recipient or options.get("recipients") or "all",
|
|
6592
|
-
"thread_id": options.get("thread_id"),
|
|
6593
|
-
"parent_id": options.get("parent_id"),
|
|
6594
|
-
"kind": "message",
|
|
6595
|
-
"message": message_text,
|
|
6596
|
-
"meta": {"source": "slash_command"},
|
|
6597
|
-
})
|
|
6598
|
-
write_anthropic_text_response(handler, model, f"Channel message posted as id {message['id']}.", stream)
|
|
6599
|
-
return True
|
|
6600
|
-
|
|
6601
|
-
write_anthropic_text_response(handler, model, "Usage: `/channel status`, `/channel poll`, `/channel wait`, or `/channel send message=\"...\"`.", stream)
|
|
6602
|
-
return True
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
6563
|
def normalize_tool_arguments(tool_name: str, args: Any) -> dict[str, Any]:
|
|
6606
6564
|
if isinstance(args, dict):
|
|
6607
6565
|
return args
|
|
@@ -8230,9 +8188,6 @@ class RouterHandler(BaseHTTPRequestHandler):
|
|
|
8230
8188
|
if maybe_handle_router_debug_request(self, body):
|
|
8231
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 ""))
|
|
8232
8190
|
return
|
|
8233
|
-
if maybe_handle_channel_bridge_request(self, body):
|
|
8234
|
-
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 ""))
|
|
8235
|
-
return
|
|
8236
8191
|
if maybe_handle_advisor_request(self, provider, pcfg, body):
|
|
8237
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 ""))
|
|
8238
8193
|
return
|
|
@@ -8469,6 +8424,38 @@ def mask_secret(value: str | None) -> str:
|
|
|
8469
8424
|
return f"{text[:4]}...{text[-4:]}"
|
|
8470
8425
|
|
|
8471
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
|
+
|
|
8472
8459
|
def stored_api_key_mask(provider: str, pcfg: dict[str, Any]) -> str:
|
|
8473
8460
|
if provider == "nvidia-hosted":
|
|
8474
8461
|
return mask_secret(nvidia_api_key())
|
|
@@ -8658,6 +8645,7 @@ def status_lines() -> list[str]:
|
|
|
8658
8645
|
*([f"request_timeout_ms: {pcfg.get('request_timeout_ms', 'default')}"] if provider in ("vllm", "nvidia-hosted", "self-hosted-nim") else []),
|
|
8659
8646
|
*([f"stream_idle_timeout_ms: {pcfg.get('stream_idle_timeout_ms', 'auto')}"] if provider in ("vllm", "nvidia-hosted", "self-hosted-nim") else []),
|
|
8660
8647
|
f"claude_model: {current_upstream_model_id(provider, pcfg) if direct_native else current_alias(cfg)}",
|
|
8648
|
+
f"channels: {channel_status_text(cfg)}",
|
|
8661
8649
|
f"router: {'bypassed for native provider compatibility' if direct_native else (('up' if router_up() else 'down') + ' ' + ROUTER_BASE)}",
|
|
8662
8650
|
f"config: {CONFIG_PATH}",
|
|
8663
8651
|
]
|
|
@@ -8751,6 +8739,143 @@ def cmd_web_fetch(args: argparse.Namespace) -> None:
|
|
|
8751
8739
|
print(f"mcp_config: {WEB_TOOLS_MCP_CONFIG}")
|
|
8752
8740
|
|
|
8753
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 is_channel_spec_tagged(spec: str) -> bool:
|
|
8763
|
+
return spec.startswith("plugin:") or spec.startswith("server:")
|
|
8764
|
+
|
|
8765
|
+
|
|
8766
|
+
def channel_development_enabled(cfg: dict[str, Any] | None = None) -> bool:
|
|
8767
|
+
cfg = cfg or load_config()
|
|
8768
|
+
return bool(cfg.setdefault("claude_code", {}).get("development_channels", False))
|
|
8769
|
+
|
|
8770
|
+
|
|
8771
|
+
def channel_status_text(cfg: dict[str, Any] | None = None) -> str:
|
|
8772
|
+
cfg = cfg or load_config()
|
|
8773
|
+
channels = channel_specs(cfg)
|
|
8774
|
+
if not channels:
|
|
8775
|
+
return "off"
|
|
8776
|
+
suffix = "; dev" if channel_development_enabled(cfg) else ""
|
|
8777
|
+
return f"{len(channels)} channel{'s' if len(channels) != 1 else ''}{suffix}"
|
|
8778
|
+
|
|
8779
|
+
|
|
8780
|
+
def set_channel_development_enabled(enabled: bool) -> list[str]:
|
|
8781
|
+
cfg = load_config()
|
|
8782
|
+
cfg.setdefault("claude_code", {})["development_channels"] = bool(enabled)
|
|
8783
|
+
save_config(cfg)
|
|
8784
|
+
return [f"Development channels: {'on' if enabled else 'off'}."]
|
|
8785
|
+
|
|
8786
|
+
|
|
8787
|
+
def add_channel_spec(spec: str, *, development: bool = False) -> list[str]:
|
|
8788
|
+
spec = spec.strip()
|
|
8789
|
+
if not spec:
|
|
8790
|
+
return ["Channel spec was empty."]
|
|
8791
|
+
if not is_channel_spec_tagged(spec):
|
|
8792
|
+
return ["Channel spec must start with plugin: or server:."]
|
|
8793
|
+
cfg = load_config()
|
|
8794
|
+
cc = cfg.setdefault("claude_code", {})
|
|
8795
|
+
channels = channel_specs(cfg)
|
|
8796
|
+
if spec not in channels:
|
|
8797
|
+
channels.append(spec)
|
|
8798
|
+
cc["channels"] = channels
|
|
8799
|
+
if development:
|
|
8800
|
+
cc["development_channels"] = True
|
|
8801
|
+
save_config(cfg)
|
|
8802
|
+
lines = [f"Channel added: {spec}."]
|
|
8803
|
+
if development:
|
|
8804
|
+
lines.append("Development channels: on.")
|
|
8805
|
+
return lines
|
|
8806
|
+
|
|
8807
|
+
|
|
8808
|
+
def remove_channel_spec(spec: str) -> list[str]:
|
|
8809
|
+
cfg = load_config()
|
|
8810
|
+
cc = cfg.setdefault("claude_code", {})
|
|
8811
|
+
before = channel_specs(cfg)
|
|
8812
|
+
after = [item for item in before if item != spec]
|
|
8813
|
+
cc["channels"] = after
|
|
8814
|
+
save_config(cfg)
|
|
8815
|
+
return [f"Channel removed: {spec}." if len(after) != len(before) else f"Channel was not configured: {spec}."]
|
|
8816
|
+
|
|
8817
|
+
|
|
8818
|
+
def clear_channel_specs() -> list[str]:
|
|
8819
|
+
cfg = load_config()
|
|
8820
|
+
cfg.setdefault("claude_code", {})["channels"] = []
|
|
8821
|
+
save_config(cfg)
|
|
8822
|
+
return ["Claude Code channels cleared."]
|
|
8823
|
+
|
|
8824
|
+
|
|
8825
|
+
def cmd_channels(args: argparse.Namespace) -> None:
|
|
8826
|
+
cfg = load_config()
|
|
8827
|
+
values = list(getattr(args, "values", []) or [])
|
|
8828
|
+
if not values:
|
|
8829
|
+
print(f"channels: {channel_status_text(cfg)}")
|
|
8830
|
+
for name, spec in OFFICIAL_CHANNEL_PLUGINS.items():
|
|
8831
|
+
mark = "*" if spec in channel_specs(cfg) else " "
|
|
8832
|
+
print(f" {mark} {name:<10} {spec}")
|
|
8833
|
+
for spec in channel_specs(cfg):
|
|
8834
|
+
if spec not in OFFICIAL_CHANNEL_PLUGINS.values():
|
|
8835
|
+
print(f" * custom {spec}")
|
|
8836
|
+
print(f"development_channels: {'on' if channel_development_enabled(cfg) else 'off'}")
|
|
8837
|
+
return
|
|
8838
|
+
head = values[0].strip().lower()
|
|
8839
|
+
if head in ("on", "enable", "add"):
|
|
8840
|
+
if len(values) < 2:
|
|
8841
|
+
raise SystemExit("Usage: claude-any channels add CHANNEL_SPEC")
|
|
8842
|
+
for line in add_channel_spec(values[1]):
|
|
8843
|
+
print(line)
|
|
8844
|
+
return
|
|
8845
|
+
if head in ("dev", "development"):
|
|
8846
|
+
if len(values) >= 2 and values[1].lower() in ("on", "off", "true", "false", "1", "0"):
|
|
8847
|
+
enabled = values[1].lower() in ("on", "true", "1")
|
|
8848
|
+
for line in set_channel_development_enabled(enabled):
|
|
8849
|
+
print(line)
|
|
8850
|
+
return
|
|
8851
|
+
if len(values) < 2:
|
|
8852
|
+
raise SystemExit("Usage: claude-any channels dev CHANNEL_SPEC | claude-any channels dev on|off")
|
|
8853
|
+
for line in add_channel_spec(values[1], development=True):
|
|
8854
|
+
print(line)
|
|
8855
|
+
return
|
|
8856
|
+
if head in ("off", "disable", "remove", "rm"):
|
|
8857
|
+
if len(values) < 2:
|
|
8858
|
+
raise SystemExit("Usage: claude-any channels remove CHANNEL_SPEC")
|
|
8859
|
+
for line in remove_channel_spec(values[1]):
|
|
8860
|
+
print(line)
|
|
8861
|
+
return
|
|
8862
|
+
if head in ("clear", "reset"):
|
|
8863
|
+
for line in clear_channel_specs():
|
|
8864
|
+
print(line)
|
|
8865
|
+
return
|
|
8866
|
+
if head in OFFICIAL_CHANNEL_PLUGINS:
|
|
8867
|
+
spec = OFFICIAL_CHANNEL_PLUGINS[head]
|
|
8868
|
+
if spec in channel_specs(cfg):
|
|
8869
|
+
for line in remove_channel_spec(spec):
|
|
8870
|
+
print(line)
|
|
8871
|
+
else:
|
|
8872
|
+
for line in add_channel_spec(spec):
|
|
8873
|
+
print(line)
|
|
8874
|
+
return
|
|
8875
|
+
for line in add_channel_spec(values[0]):
|
|
8876
|
+
print(line)
|
|
8877
|
+
|
|
8878
|
+
|
|
8754
8879
|
def cmd_ollama_native(args: argparse.Namespace) -> None:
|
|
8755
8880
|
cfg = load_config()
|
|
8756
8881
|
pcfg = cfg["providers"]["ollama"]
|
|
@@ -11937,8 +12062,9 @@ def main_menu_rows(cfg: dict[str, Any], provider: str, pcfg: dict[str, Any], lan
|
|
|
11937
12062
|
f"4. {ui_text('model', lang)} [{compact_text(pcfg.get('current_model', 'unset'), 62)}]",
|
|
11938
12063
|
f"5. {ui_text('advisor_model', lang)} [{compact_text(pcfg.get('advisor_model') or 'off', 62)}]",
|
|
11939
12064
|
f"6. {ui_text('options', lang)} [{compact_text(llm_options_status(provider, pcfg), 62)}]",
|
|
11940
|
-
f"7. {
|
|
11941
|
-
f"8. {ui_text('
|
|
12065
|
+
f"7. Channels [{channel_status_text(cfg)}]",
|
|
12066
|
+
f"8. {ui_text('test', lang)}",
|
|
12067
|
+
f"9. {ui_text('launch', lang)}",
|
|
11942
12068
|
ui_text("quit", lang),
|
|
11943
12069
|
]
|
|
11944
12070
|
|
|
@@ -12010,6 +12136,29 @@ def advisor_model_panel_rows(provider: str, pcfg: dict[str, Any]) -> tuple[list[
|
|
|
12010
12136
|
return rows, deduped_values
|
|
12011
12137
|
|
|
12012
12138
|
|
|
12139
|
+
def channel_panel_rows(cfg: dict[str, Any]) -> tuple[list[str], list[str]]:
|
|
12140
|
+
channels = channel_specs(cfg)
|
|
12141
|
+
dev_enabled = channel_development_enabled(cfg)
|
|
12142
|
+
rows: list[str] = []
|
|
12143
|
+
values: list[str] = []
|
|
12144
|
+
rows.append(f"Development channel loading [{'on' if dev_enabled else 'off'}]")
|
|
12145
|
+
values.append("__toggle_dev__")
|
|
12146
|
+
for name, spec in OFFICIAL_CHANNEL_PLUGINS.items():
|
|
12147
|
+
mark = "*" if spec in channels else " "
|
|
12148
|
+
rows.append(f"{mark} {name:<10} {spec}")
|
|
12149
|
+
values.append(spec)
|
|
12150
|
+
rows.append("+ Add development/custom channel...")
|
|
12151
|
+
values.append("__add_custom__")
|
|
12152
|
+
if channels:
|
|
12153
|
+
rows.append("- Remove channel...")
|
|
12154
|
+
values.append("__remove__")
|
|
12155
|
+
rows.append("Clear all channels")
|
|
12156
|
+
values.append("__clear__")
|
|
12157
|
+
rows.append("Back")
|
|
12158
|
+
values.append("back")
|
|
12159
|
+
return rows, values
|
|
12160
|
+
|
|
12161
|
+
|
|
12013
12162
|
def api_key_panel_rows(provider: str) -> tuple[list[str], list[str]]:
|
|
12014
12163
|
rows = [
|
|
12015
12164
|
"Type or paste API key as hidden input",
|
|
@@ -12310,7 +12459,7 @@ def portable_language_menu() -> int:
|
|
|
12310
12459
|
|
|
12311
12460
|
def portable_prelaunch_menu() -> int:
|
|
12312
12461
|
enable_ansi()
|
|
12313
|
-
main_idx =
|
|
12462
|
+
main_idx = 9 if settings_ready_except_api_key() else 0
|
|
12314
12463
|
panel: str | None = None
|
|
12315
12464
|
panel_idx = 0
|
|
12316
12465
|
panel_rows: list[str] = []
|
|
@@ -12352,6 +12501,8 @@ def portable_prelaunch_menu() -> int:
|
|
|
12352
12501
|
panel_rows, panel_values = ["Run compatibility test", "Back"], ["run", "back"]
|
|
12353
12502
|
elif name == "options":
|
|
12354
12503
|
panel_rows, panel_values = llm_option_panel_rows(provider, pcfg, cfg.get("language", "en"))
|
|
12504
|
+
elif name == "channels":
|
|
12505
|
+
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
12355
12506
|
elif name == "context":
|
|
12356
12507
|
panel_rows, panel_values = context_setup_panel_rows(provider, pcfg, cfg.get("language", "en"))
|
|
12357
12508
|
elif name == "preset":
|
|
@@ -12528,7 +12679,40 @@ def portable_prelaunch_menu() -> int:
|
|
|
12528
12679
|
messages = lines[-8:] if lines else ["Test produced no output."]
|
|
12529
12680
|
panel_rows, panel_values = ["Run compatibility test again", "Back"], ["run", "back"]
|
|
12530
12681
|
refresh_checks()
|
|
12531
|
-
main_idx =
|
|
12682
|
+
main_idx = 9 if "Compatibility: OK" in out else 4
|
|
12683
|
+
elif panel == "channels":
|
|
12684
|
+
if value == "back":
|
|
12685
|
+
close_panel()
|
|
12686
|
+
elif value == "__toggle_dev__":
|
|
12687
|
+
messages = set_channel_development_enabled(not channel_development_enabled(cfg))
|
|
12688
|
+
cfg = load_config()
|
|
12689
|
+
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
12690
|
+
panel_idx = 0
|
|
12691
|
+
elif value == "__add_custom__":
|
|
12692
|
+
spec = prompt_menu_value("Channel spec (for example plugin:ainet@local or server:ainet)", restore_tty=restore_line_mode, raw_tty=restore_raw_mode)
|
|
12693
|
+
if spec:
|
|
12694
|
+
messages = add_channel_spec(spec, development=True)
|
|
12695
|
+
cfg = load_config()
|
|
12696
|
+
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
12697
|
+
elif value == "__remove__":
|
|
12698
|
+
spec = prompt_menu_value("Channel spec to remove", "", restore_tty=restore_line_mode, raw_tty=restore_raw_mode)
|
|
12699
|
+
if spec:
|
|
12700
|
+
messages = remove_channel_spec(spec)
|
|
12701
|
+
cfg = load_config()
|
|
12702
|
+
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
12703
|
+
elif value == "__clear__":
|
|
12704
|
+
messages = clear_channel_specs()
|
|
12705
|
+
cfg = load_config()
|
|
12706
|
+
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
12707
|
+
panel_idx = 0
|
|
12708
|
+
elif value:
|
|
12709
|
+
if value in channel_specs(cfg):
|
|
12710
|
+
messages = remove_channel_spec(value)
|
|
12711
|
+
else:
|
|
12712
|
+
messages = add_channel_spec(value)
|
|
12713
|
+
cfg = load_config()
|
|
12714
|
+
panel_rows, panel_values = channel_panel_rows(cfg)
|
|
12715
|
+
refresh_checks()
|
|
12532
12716
|
elif panel == "options":
|
|
12533
12717
|
if value == "back":
|
|
12534
12718
|
close_panel()
|
|
@@ -12627,13 +12811,13 @@ def portable_prelaunch_menu() -> int:
|
|
|
12627
12811
|
continue
|
|
12628
12812
|
|
|
12629
12813
|
if key in ("up", "k"):
|
|
12630
|
-
main_idx = (main_idx - 1) %
|
|
12814
|
+
main_idx = (main_idx - 1) % 11
|
|
12631
12815
|
elif key in ("down", "j"):
|
|
12632
|
-
main_idx = (main_idx + 1) %
|
|
12816
|
+
main_idx = (main_idx + 1) % 11
|
|
12633
12817
|
elif key in ("esc", "q"):
|
|
12634
12818
|
return 10
|
|
12635
12819
|
elif key == "enter":
|
|
12636
|
-
actions = ["language", "provider", "api-key", "base-url", "model", "advisor-model", "options", "test", "launch", "quit"]
|
|
12820
|
+
actions = ["language", "provider", "api-key", "base-url", "model", "advisor-model", "options", "channels", "test", "launch", "quit"]
|
|
12637
12821
|
action = actions[main_idx]
|
|
12638
12822
|
if action == "launch":
|
|
12639
12823
|
blockers = launch_readiness_errors()
|
|
@@ -12722,6 +12906,15 @@ def has_passthrough_option(passthrough: list[str], *names: str) -> bool:
|
|
|
12722
12906
|
return any(arg in names or any(arg.startswith(name + "=") for name in names) for arg in passthrough)
|
|
12723
12907
|
|
|
12724
12908
|
|
|
12909
|
+
def claude_channel_args(cfg: dict[str, Any], passthrough: list[str]) -> list[str]:
|
|
12910
|
+
channels = [spec for spec in channel_specs(cfg) if is_channel_spec_tagged(spec)]
|
|
12911
|
+
if not channels or has_passthrough_option(passthrough, "--channels", "--dangerously-load-development-channels"):
|
|
12912
|
+
return []
|
|
12913
|
+
if channel_development_enabled(cfg):
|
|
12914
|
+
return ["--dangerously-load-development-channels", *channels]
|
|
12915
|
+
return ["--channels", *channels]
|
|
12916
|
+
|
|
12917
|
+
|
|
12725
12918
|
def write_web_tools_mcp_config(cfg: dict[str, Any]) -> Path:
|
|
12726
12919
|
web = cfg.get("web_search", {})
|
|
12727
12920
|
package = web.get("package") or "ddg-mcp-search"
|
|
@@ -13061,6 +13254,7 @@ def launch_claude(
|
|
|
13061
13254
|
extra_args.extend(["--mcp-config", str(write_duckduckgo_mcp_config(cfg))])
|
|
13062
13255
|
if should_append_compat_prompt(provider, cfg) and not has_passthrough_option(passthrough, "--system-prompt"):
|
|
13063
13256
|
extra_args.extend(["--append-system-prompt", NON_ANTHROPIC_COMPAT_PROMPT])
|
|
13257
|
+
extra_args.extend(claude_channel_args(cfg, passthrough))
|
|
13064
13258
|
cmd = [
|
|
13065
13259
|
claude,
|
|
13066
13260
|
"--dangerously-skip-permissions",
|
|
@@ -13091,6 +13285,7 @@ Control plane, runs before Claude Code and does not require LLM connectivity:
|
|
|
13091
13285
|
claude-any set-api-key PROVIDER KEY
|
|
13092
13286
|
claude-any web-search [on|off] Auto-attach DuckDuckGo MCP for non-native providers
|
|
13093
13287
|
claude-any web-fetch [on|off] Auto-attach fetch MCP for web page content
|
|
13288
|
+
claude-any channels [cmd] Configure Claude Code --channels auto-injection
|
|
13094
13289
|
claude-any ollama-native [on|off] Use Ollama's official Claude Code env path
|
|
13095
13290
|
claude-any ollama-options [provider] [key=value ...]
|
|
13096
13291
|
Set Ollama num_ctx/options/keep_alive/think
|
|
@@ -13129,6 +13324,11 @@ Headless setup flags, namespaced to avoid Claude CLI collisions:
|
|
|
13129
13324
|
claude-any --ca-no-web-search Disable DuckDuckGo MCP for this launch
|
|
13130
13325
|
claude-any --ca-web-fetch Enable fetch MCP
|
|
13131
13326
|
claude-any --ca-no-web-fetch Disable fetch MCP
|
|
13327
|
+
claude-any --ca-channel SPEC Add an official/approved Claude Code channel
|
|
13328
|
+
claude-any --ca-dev-channel SPEC Add a development channel and enable dev loading
|
|
13329
|
+
claude-any --ca-development-channels on|off
|
|
13330
|
+
Use tagged specs with --dangerously-load-development-channels
|
|
13331
|
+
claude-any --ca-clear-channels Clear saved channel auto-injection specs
|
|
13132
13332
|
claude-any --ca-no-self-update-check
|
|
13133
13333
|
Skip Claude Any npm self-update check
|
|
13134
13334
|
claude-any --ca-no-update-check Skip Claude Code update check for this launch
|
|
@@ -13236,6 +13436,28 @@ def apply_headless_env_config() -> tuple[bool, bool | None, bool | None, bool |
|
|
|
13236
13436
|
if ollama_values:
|
|
13237
13437
|
cmd_ollama_options(argparse.Namespace(values=ollama_values))
|
|
13238
13438
|
skip_menu = True
|
|
13439
|
+
channel_values = [
|
|
13440
|
+
item.strip()
|
|
13441
|
+
for item in re.split(r"[\s,]+", os.environ.get("CLAUDE_ANY_CHANNELS", "").strip())
|
|
13442
|
+
if item.strip()
|
|
13443
|
+
]
|
|
13444
|
+
for channel_value in channel_values:
|
|
13445
|
+
add_channel_spec(channel_value)
|
|
13446
|
+
skip_menu = True
|
|
13447
|
+
dev_channel_values = [
|
|
13448
|
+
item.strip()
|
|
13449
|
+
for item in re.split(r"[\s,]+", os.environ.get("CLAUDE_ANY_DEV_CHANNELS", "").strip())
|
|
13450
|
+
if item.strip()
|
|
13451
|
+
]
|
|
13452
|
+
for channel_value in dev_channel_values:
|
|
13453
|
+
add_channel_spec(channel_value, development=True)
|
|
13454
|
+
skip_menu = True
|
|
13455
|
+
dev_channels = os.environ.get("CLAUDE_ANY_DEVELOPMENT_CHANNELS", "").strip().lower()
|
|
13456
|
+
if dev_channels:
|
|
13457
|
+
if dev_channels not in ("on", "off", "true", "false", "1", "0"):
|
|
13458
|
+
raise SystemExit("CLAUDE_ANY_DEVELOPMENT_CHANNELS must be on or off")
|
|
13459
|
+
set_channel_development_enabled(dev_channels in ("on", "true", "1"))
|
|
13460
|
+
skip_menu = True
|
|
13239
13461
|
return skip_menu, web_search_override, update_check_override, self_update_check_override, force_menu
|
|
13240
13462
|
|
|
13241
13463
|
|
|
@@ -13295,6 +13517,9 @@ def run_cli(argv: list[str]) -> int:
|
|
|
13295
13517
|
if head in ("web-fetch", "webfetch"):
|
|
13296
13518
|
cmd_web_fetch(argparse.Namespace(value=rest[0] if rest else None))
|
|
13297
13519
|
return 0
|
|
13520
|
+
if head in ("channels", "channel"):
|
|
13521
|
+
cmd_channels(argparse.Namespace(values=rest))
|
|
13522
|
+
return 0
|
|
13298
13523
|
if head in ("ollama-native", "ollama-compat"):
|
|
13299
13524
|
cmd_ollama_native(argparse.Namespace(value=rest[0] if rest else None))
|
|
13300
13525
|
return 0
|
|
@@ -13629,6 +13854,48 @@ def run_cli(argv: list[str]) -> int:
|
|
|
13629
13854
|
cmd_web_fetch(argparse.Namespace(value="off"))
|
|
13630
13855
|
skip_menu = True
|
|
13631
13856
|
i += 1
|
|
13857
|
+
elif arg == "--ca-channel" or arg.startswith("--ca-channel="):
|
|
13858
|
+
value = arg.split("=", 1)[1] if "=" in arg else None
|
|
13859
|
+
if value is None:
|
|
13860
|
+
if i + 1 >= len(argv):
|
|
13861
|
+
raise SystemExit("Missing channel spec for --ca-channel")
|
|
13862
|
+
value = argv[i + 1]
|
|
13863
|
+
i += 2
|
|
13864
|
+
else:
|
|
13865
|
+
i += 1
|
|
13866
|
+
for line in add_channel_spec(value):
|
|
13867
|
+
print(line)
|
|
13868
|
+
skip_menu = True
|
|
13869
|
+
elif arg == "--ca-dev-channel" or arg.startswith("--ca-dev-channel="):
|
|
13870
|
+
value = arg.split("=", 1)[1] if "=" in arg else None
|
|
13871
|
+
if value is None:
|
|
13872
|
+
if i + 1 >= len(argv):
|
|
13873
|
+
raise SystemExit("Missing channel spec for --ca-dev-channel")
|
|
13874
|
+
value = argv[i + 1]
|
|
13875
|
+
i += 2
|
|
13876
|
+
else:
|
|
13877
|
+
i += 1
|
|
13878
|
+
for line in add_channel_spec(value, development=True):
|
|
13879
|
+
print(line)
|
|
13880
|
+
skip_menu = True
|
|
13881
|
+
elif arg == "--ca-development-channels" or arg.startswith("--ca-development-channels="):
|
|
13882
|
+
value = arg.split("=", 1)[1] if "=" in arg else None
|
|
13883
|
+
if value is None:
|
|
13884
|
+
if i + 1 >= len(argv):
|
|
13885
|
+
raise SystemExit("Missing on/off for --ca-development-channels")
|
|
13886
|
+
value = argv[i + 1]
|
|
13887
|
+
i += 2
|
|
13888
|
+
else:
|
|
13889
|
+
i += 1
|
|
13890
|
+
enabled = value.strip().lower() in ("on", "enable", "enabled", "true", "1")
|
|
13891
|
+
for line in set_channel_development_enabled(enabled):
|
|
13892
|
+
print(line)
|
|
13893
|
+
skip_menu = True
|
|
13894
|
+
elif arg == "--ca-clear-channels":
|
|
13895
|
+
for line in clear_channel_specs():
|
|
13896
|
+
print(line)
|
|
13897
|
+
skip_menu = True
|
|
13898
|
+
i += 1
|
|
13632
13899
|
elif arg == "--ca-no-update-check":
|
|
13633
13900
|
update_check = False
|
|
13634
13901
|
skip_menu = True
|
|
@@ -13699,6 +13966,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
13699
13966
|
wf = sub.add_parser("web-fetch")
|
|
13700
13967
|
wf.add_argument("value", nargs="?")
|
|
13701
13968
|
wf.set_defaults(func=cmd_web_fetch)
|
|
13969
|
+
ch = sub.add_parser("channels")
|
|
13970
|
+
ch.add_argument("values", nargs="*")
|
|
13971
|
+
ch.set_defaults(func=cmd_channels)
|
|
13702
13972
|
on = sub.add_parser("ollama-native")
|
|
13703
13973
|
on.add_argument("value", nargs="?")
|
|
13704
13974
|
on.set_defaults(func=cmd_ollama_native)
|
package/package.json
CHANGED