@oneciel-ai/claude-any 0.1.71 → 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/claude_any.py +428 -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,137 @@ 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 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
|
+
|
|
8754
8873
|
def cmd_ollama_native(args: argparse.Namespace) -> None:
|
|
8755
8874
|
cfg = load_config()
|
|
8756
8875
|
pcfg = cfg["providers"]["ollama"]
|
|
@@ -11937,8 +12056,9 @@ def main_menu_rows(cfg: dict[str, Any], provider: str, pcfg: dict[str, Any], lan
|
|
|
11937
12056
|
f"4. {ui_text('model', lang)} [{compact_text(pcfg.get('current_model', 'unset'), 62)}]",
|
|
11938
12057
|
f"5. {ui_text('advisor_model', lang)} [{compact_text(pcfg.get('advisor_model') or 'off', 62)}]",
|
|
11939
12058
|
f"6. {ui_text('options', lang)} [{compact_text(llm_options_status(provider, pcfg), 62)}]",
|
|
11940
|
-
f"7. {
|
|
11941
|
-
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)}",
|
|
11942
12062
|
ui_text("quit", lang),
|
|
11943
12063
|
]
|
|
11944
12064
|
|
|
@@ -12010,6 +12130,29 @@ def advisor_model_panel_rows(provider: str, pcfg: dict[str, Any]) -> tuple[list[
|
|
|
12010
12130
|
return rows, deduped_values
|
|
12011
12131
|
|
|
12012
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
|
+
|
|
12013
12156
|
def api_key_panel_rows(provider: str) -> tuple[list[str], list[str]]:
|
|
12014
12157
|
rows = [
|
|
12015
12158
|
"Type or paste API key as hidden input",
|
|
@@ -12310,7 +12453,7 @@ def portable_language_menu() -> int:
|
|
|
12310
12453
|
|
|
12311
12454
|
def portable_prelaunch_menu() -> int:
|
|
12312
12455
|
enable_ansi()
|
|
12313
|
-
main_idx =
|
|
12456
|
+
main_idx = 9 if settings_ready_except_api_key() else 0
|
|
12314
12457
|
panel: str | None = None
|
|
12315
12458
|
panel_idx = 0
|
|
12316
12459
|
panel_rows: list[str] = []
|
|
@@ -12352,6 +12495,8 @@ def portable_prelaunch_menu() -> int:
|
|
|
12352
12495
|
panel_rows, panel_values = ["Run compatibility test", "Back"], ["run", "back"]
|
|
12353
12496
|
elif name == "options":
|
|
12354
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)
|
|
12355
12500
|
elif name == "context":
|
|
12356
12501
|
panel_rows, panel_values = context_setup_panel_rows(provider, pcfg, cfg.get("language", "en"))
|
|
12357
12502
|
elif name == "preset":
|
|
@@ -12528,7 +12673,40 @@ def portable_prelaunch_menu() -> int:
|
|
|
12528
12673
|
messages = lines[-8:] if lines else ["Test produced no output."]
|
|
12529
12674
|
panel_rows, panel_values = ["Run compatibility test again", "Back"], ["run", "back"]
|
|
12530
12675
|
refresh_checks()
|
|
12531
|
-
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()
|
|
12532
12710
|
elif panel == "options":
|
|
12533
12711
|
if value == "back":
|
|
12534
12712
|
close_panel()
|
|
@@ -12627,13 +12805,13 @@ def portable_prelaunch_menu() -> int:
|
|
|
12627
12805
|
continue
|
|
12628
12806
|
|
|
12629
12807
|
if key in ("up", "k"):
|
|
12630
|
-
main_idx = (main_idx - 1) %
|
|
12808
|
+
main_idx = (main_idx - 1) % 11
|
|
12631
12809
|
elif key in ("down", "j"):
|
|
12632
|
-
main_idx = (main_idx + 1) %
|
|
12810
|
+
main_idx = (main_idx + 1) % 11
|
|
12633
12811
|
elif key in ("esc", "q"):
|
|
12634
12812
|
return 10
|
|
12635
12813
|
elif key == "enter":
|
|
12636
|
-
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"]
|
|
12637
12815
|
action = actions[main_idx]
|
|
12638
12816
|
if action == "launch":
|
|
12639
12817
|
blockers = launch_readiness_errors()
|
|
@@ -12722,6 +12900,18 @@ def has_passthrough_option(passthrough: list[str], *names: str) -> bool:
|
|
|
12722
12900
|
return any(arg in names or any(arg.startswith(name + "=") for name in names) for arg in passthrough)
|
|
12723
12901
|
|
|
12724
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
|
+
|
|
12725
12915
|
def write_web_tools_mcp_config(cfg: dict[str, Any]) -> Path:
|
|
12726
12916
|
web = cfg.get("web_search", {})
|
|
12727
12917
|
package = web.get("package") or "ddg-mcp-search"
|
|
@@ -13061,6 +13251,7 @@ def launch_claude(
|
|
|
13061
13251
|
extra_args.extend(["--mcp-config", str(write_duckduckgo_mcp_config(cfg))])
|
|
13062
13252
|
if should_append_compat_prompt(provider, cfg) and not has_passthrough_option(passthrough, "--system-prompt"):
|
|
13063
13253
|
extra_args.extend(["--append-system-prompt", NON_ANTHROPIC_COMPAT_PROMPT])
|
|
13254
|
+
extra_args.extend(claude_channel_args(cfg, passthrough))
|
|
13064
13255
|
cmd = [
|
|
13065
13256
|
claude,
|
|
13066
13257
|
"--dangerously-skip-permissions",
|
|
@@ -13091,6 +13282,7 @@ Control plane, runs before Claude Code and does not require LLM connectivity:
|
|
|
13091
13282
|
claude-any set-api-key PROVIDER KEY
|
|
13092
13283
|
claude-any web-search [on|off] Auto-attach DuckDuckGo MCP for non-native providers
|
|
13093
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
|
|
13094
13286
|
claude-any ollama-native [on|off] Use Ollama's official Claude Code env path
|
|
13095
13287
|
claude-any ollama-options [provider] [key=value ...]
|
|
13096
13288
|
Set Ollama num_ctx/options/keep_alive/think
|
|
@@ -13129,6 +13321,11 @@ Headless setup flags, namespaced to avoid Claude CLI collisions:
|
|
|
13129
13321
|
claude-any --ca-no-web-search Disable DuckDuckGo MCP for this launch
|
|
13130
13322
|
claude-any --ca-web-fetch Enable fetch MCP
|
|
13131
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
|
|
13132
13329
|
claude-any --ca-no-self-update-check
|
|
13133
13330
|
Skip Claude Any npm self-update check
|
|
13134
13331
|
claude-any --ca-no-update-check Skip Claude Code update check for this launch
|
|
@@ -13236,6 +13433,28 @@ def apply_headless_env_config() -> tuple[bool, bool | None, bool | None, bool |
|
|
|
13236
13433
|
if ollama_values:
|
|
13237
13434
|
cmd_ollama_options(argparse.Namespace(values=ollama_values))
|
|
13238
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
|
|
13239
13458
|
return skip_menu, web_search_override, update_check_override, self_update_check_override, force_menu
|
|
13240
13459
|
|
|
13241
13460
|
|
|
@@ -13295,6 +13514,9 @@ def run_cli(argv: list[str]) -> int:
|
|
|
13295
13514
|
if head in ("web-fetch", "webfetch"):
|
|
13296
13515
|
cmd_web_fetch(argparse.Namespace(value=rest[0] if rest else None))
|
|
13297
13516
|
return 0
|
|
13517
|
+
if head in ("channels", "channel"):
|
|
13518
|
+
cmd_channels(argparse.Namespace(values=rest))
|
|
13519
|
+
return 0
|
|
13298
13520
|
if head in ("ollama-native", "ollama-compat"):
|
|
13299
13521
|
cmd_ollama_native(argparse.Namespace(value=rest[0] if rest else None))
|
|
13300
13522
|
return 0
|
|
@@ -13629,6 +13851,48 @@ def run_cli(argv: list[str]) -> int:
|
|
|
13629
13851
|
cmd_web_fetch(argparse.Namespace(value="off"))
|
|
13630
13852
|
skip_menu = True
|
|
13631
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
|
|
13632
13896
|
elif arg == "--ca-no-update-check":
|
|
13633
13897
|
update_check = False
|
|
13634
13898
|
skip_menu = True
|
|
@@ -13699,6 +13963,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
13699
13963
|
wf = sub.add_parser("web-fetch")
|
|
13700
13964
|
wf.add_argument("value", nargs="?")
|
|
13701
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)
|
|
13702
13969
|
on = sub.add_parser("ollama-native")
|
|
13703
13970
|
on.add_argument("value", nargs="?")
|
|
13704
13971
|
on.set_defaults(func=cmd_ollama_native)
|
package/package.json
CHANGED