kctl-telegram 0.6.3__tar.gz

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.
Files changed (31) hide show
  1. kctl_telegram-0.6.3/.gitignore +33 -0
  2. kctl_telegram-0.6.3/PKG-INFO +17 -0
  3. kctl_telegram-0.6.3/README.md +71 -0
  4. kctl_telegram-0.6.3/pyproject.toml +45 -0
  5. kctl_telegram-0.6.3/skills/telegram-admin/SKILL.md +151 -0
  6. kctl_telegram-0.6.3/src/kctl_telegram/__init__.py +5 -0
  7. kctl_telegram-0.6.3/src/kctl_telegram/__main__.py +7 -0
  8. kctl_telegram-0.6.3/src/kctl_telegram/cli.py +131 -0
  9. kctl_telegram-0.6.3/src/kctl_telegram/commands/__init__.py +1 -0
  10. kctl_telegram-0.6.3/src/kctl_telegram/commands/bots.py +175 -0
  11. kctl_telegram-0.6.3/src/kctl_telegram/commands/chatwoot.py +105 -0
  12. kctl_telegram-0.6.3/src/kctl_telegram/commands/config_cmd.py +512 -0
  13. kctl_telegram-0.6.3/src/kctl_telegram/commands/dashboard.py +133 -0
  14. kctl_telegram-0.6.3/src/kctl_telegram/commands/doctor_cmd.py +57 -0
  15. kctl_telegram-0.6.3/src/kctl_telegram/commands/groups.py +125 -0
  16. kctl_telegram-0.6.3/src/kctl_telegram/commands/health.py +177 -0
  17. kctl_telegram-0.6.3/src/kctl_telegram/commands/messages.py +161 -0
  18. kctl_telegram-0.6.3/src/kctl_telegram/commands/skill_cmd.py +76 -0
  19. kctl_telegram-0.6.3/src/kctl_telegram/core/__init__.py +1 -0
  20. kctl_telegram-0.6.3/src/kctl_telegram/core/callbacks.py +33 -0
  21. kctl_telegram-0.6.3/src/kctl_telegram/core/client.py +76 -0
  22. kctl_telegram-0.6.3/src/kctl_telegram/core/config.py +119 -0
  23. kctl_telegram-0.6.3/src/kctl_telegram/core/exceptions.py +30 -0
  24. kctl_telegram-0.6.3/src/kctl_telegram/core/output.py +10 -0
  25. kctl_telegram-0.6.3/tests/conftest.py +60 -0
  26. kctl_telegram-0.6.3/tests/test_bots.py +244 -0
  27. kctl_telegram-0.6.3/tests/test_config.py +261 -0
  28. kctl_telegram-0.6.3/tests/test_groups.py +165 -0
  29. kctl_telegram-0.6.3/tests/test_health.py +139 -0
  30. kctl_telegram-0.6.3/tests/test_messages.py +289 -0
  31. kctl_telegram-0.6.3/tests/test_smoke.py +15 -0
@@ -0,0 +1,33 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ *.egg
6
+ dist/
7
+ build/
8
+ .eggs/
9
+
10
+ # Virtual environments
11
+ .venv/
12
+ venv/
13
+
14
+ # IDE
15
+ .idea/
16
+ .vscode/
17
+ *.swp
18
+ *.swo
19
+
20
+ # Testing
21
+ .pytest_cache/
22
+ .coverage
23
+ htmlcov/
24
+ .mypy_cache/
25
+ .ruff_cache/
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Environment
32
+ .env
33
+ .env.local
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: kctl-telegram
3
+ Version: 0.6.3
4
+ Summary: Kodemeio Telegram CLI — bot platform management
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: httpx>=0.28.0
7
+ Requires-Dist: kctl-lib>=0.8.0
8
+ Requires-Dist: pydantic>=2.10.0
9
+ Requires-Dist: pyyaml>=6.0.2
10
+ Requires-Dist: rich>=13.9.0
11
+ Requires-Dist: typer>=0.15.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: mypy>=1.14.0; extra == 'dev'
14
+ Requires-Dist: pytest-httpx>=0.35.0; extra == 'dev'
15
+ Requires-Dist: pytest>=8.3.0; extra == 'dev'
16
+ Requires-Dist: ruff>=0.9.0; extra == 'dev'
17
+ Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
@@ -0,0 +1,71 @@
1
+ # kctl-telegram
2
+
3
+ Kodemeio Telegram CLI — manage the multi-bot Telegram notification platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uv tool install .
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ kctl-telegram config init
15
+ kctl-telegram health
16
+ kctl-telegram dashboard
17
+ kctl-telegram bots list
18
+ ```
19
+
20
+ ## Command Groups
21
+
22
+ | Group | Description |
23
+ |-------|-------------|
24
+ | `config` | Manage CLI configuration and profiles |
25
+ | `health` | Health checks and diagnostics |
26
+ | `dashboard` | System overview dashboard |
27
+ | `bots` | Manage Telegram bots |
28
+ | `groups` | Manage Telegram groups |
29
+ | `messages` | Send and manage messages |
30
+ | `chatwoot` | Manage Chatwoot integrations |
31
+
32
+ ## Global Options
33
+
34
+ | Option | Short | Description |
35
+ |--------|-------|-------------|
36
+ | `--json` | | Output as JSON |
37
+ | `--quiet` | `-q` | Suppress info messages |
38
+ | `--format` | `-f` | Output format: pretty, json, csv, yaml |
39
+ | `--no-header` | | Suppress table headers (csv) |
40
+ | `--profile` | `-p` | Config profile name |
41
+ | `--url` | | API URL override |
42
+ | `--api-key` | | API key override |
43
+ | `--version` | `-V` | Show version and exit |
44
+
45
+ ## Configuration
46
+
47
+ Config lives in `~/.config/kodemeio/config.yaml` under the `telegram` service key.
48
+
49
+ ```bash
50
+ # Initialize default profile
51
+ kctl-telegram config init
52
+
53
+ # Add a named profile
54
+ kctl-telegram config add prod \
55
+ --url https://telegram-api.example.com \
56
+ --api-key $TELEGRAM_API_KEY
57
+
58
+ # Switch active profile
59
+ kctl-telegram config use prod
60
+
61
+ # Show current profile (key masked)
62
+ kctl-telegram config show
63
+ ```
64
+
65
+ ## Development
66
+
67
+ ```bash
68
+ uv run pytest tests/ -v
69
+ uv run ruff check src/
70
+ uv run mypy src/
71
+ ```
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "kctl-telegram"
7
+ version = "0.6.3"
8
+ description = "Kodemeio Telegram CLI — bot platform management"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "kctl-lib>=0.8.0",
12
+ "typer>=0.15.0",
13
+ "rich>=13.9.0",
14
+ "pydantic>=2.10.0",
15
+ "pyyaml>=6.0.2",
16
+ "httpx>=0.28.0",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest>=8.3.0",
22
+ "pytest-httpx>=0.35.0",
23
+ "ruff>=0.9.0",
24
+ "mypy>=1.14.0",
25
+ "types-PyYAML>=6.0.0",
26
+ ]
27
+
28
+ [project.scripts]
29
+ kctl-telegram = "kctl_telegram.cli:_run"
30
+
31
+ [tool.uv.sources]
32
+ kctl-lib = { workspace = true }
33
+
34
+ [project.entry-points."kctl_telegram.plugins"]
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/kctl_telegram"]
38
+
39
+ [tool.ruff]
40
+ target-version = "py312"
41
+ line-length = 120
42
+
43
+ [tool.mypy]
44
+ python_version = "3.12"
45
+ strict = true
@@ -0,0 +1,151 @@
1
+ ---
2
+ name: telegram-admin
3
+ description: >
4
+ Kodemeio Telegram bot platform administration. Supports multiple instances
5
+ via profiles. Covers multi-bot management, group tracking, message sending/
6
+ broadcasting/scheduling, Chatwoot inbox integration, health monitoring,
7
+ and dashboard overview. Use when working with kctl-telegram CLI or managing
8
+ the kodemeio-telegram FastAPI service.
9
+ version: 1.0.0
10
+ allowed-tools:
11
+ - Bash
12
+ - Read
13
+ - Write
14
+ - Edit
15
+ - Glob
16
+ - Grep
17
+ ---
18
+
19
+ # Telegram Administration
20
+
21
+ ## Managed Instances
22
+
23
+ kctl-telegram supports multiple instances via profiles:
24
+
25
+ | Profile | URL | Use |
26
+ |---|---|---|
27
+ | `kodemeio` | https://telegram.kodeme.io | Kodemeio notification bots |
28
+
29
+ ```bash
30
+ # Target a specific instance
31
+ kctl-telegram -p kodemeio bots list
32
+ kctl-telegram -p kodemeio health
33
+
34
+ # Switch default profile
35
+ kctl-telegram config use kodemeio
36
+ ```
37
+
38
+ Config: `~/.config/kodemeio/config.yaml`
39
+
40
+ ## CLI Tool: kctl-telegram
41
+
42
+ Installed globally via `uv tool install ./cli`. Run `kctl-telegram` from anywhere.
43
+
44
+ ### Global Options
45
+
46
+ ```bash
47
+ kctl-telegram [--json] [--quiet] [--profile NAME] [--url URL] [--api-key KEY] <command>
48
+ ```
49
+
50
+ - `--profile / -p`: target a specific instance
51
+ - `--url`: override API base URL
52
+ - `--api-key`: override API key
53
+ - `--json`: output as JSON (for scripting/piping)
54
+ - `--quiet / -q`: suppress info messages
55
+
56
+ ## Multi-Instance Management
57
+
58
+ ```bash
59
+ kctl-telegram config init # Interactive setup
60
+ kctl-telegram config add <name> --url <url> [--api-key KEY] # Add/update profile
61
+ kctl-telegram config use <name> # Switch default
62
+ kctl-telegram config remove <name> [--service-only] [--force] # Remove profile
63
+ kctl-telegram config profiles # List all with status
64
+ kctl-telegram config current # Show active + connection
65
+ kctl-telegram config show # Full config (masked)
66
+ kctl-telegram config set <key> <value> # Edit config
67
+ kctl-telegram config test # Test connection
68
+ kctl-telegram config migrate # Migrate flat -> scoped format
69
+ ```
70
+
71
+ ## Health & Dashboard
72
+
73
+ ```bash
74
+ kctl-telegram health [--watch] [--interval 10] # Health score (0-100)
75
+ kctl-telegram dashboard [--watch] [--interval 10] [--compact] # System overview
76
+ ```
77
+
78
+ Health scoring: API health (30pts) + readiness (30pts) + bots exist (20pts) + groups exist (20pts).
79
+
80
+ ## Bot Management
81
+
82
+ ```bash
83
+ kctl-telegram bots list # All registered bots
84
+ kctl-telegram bots get <id> # Bot details
85
+ kctl-telegram bots add --token <BOT_TOKEN> [--display-name NAME] # Register new bot
86
+ kctl-telegram bots update <id> [--display-name] [--is-active] # Update bot
87
+ kctl-telegram bots remove <id> [--force] # Deactivate bot
88
+ ```
89
+
90
+ ## Group Management
91
+
92
+ ```bash
93
+ kctl-telegram groups list # All tracked groups
94
+ kctl-telegram groups get <id> # Group details
95
+ kctl-telegram groups update <id> --field <f> --value <v> # Update group settings
96
+ ```
97
+
98
+ ## Message Operations
99
+
100
+ ```bash
101
+ kctl-telegram messages send --chat-id ID --text "msg" [--bot-id] [--parse-mode] # Send message
102
+ kctl-telegram messages broadcast --text "msg" [--bot-id] [--parse-mode] # Broadcast to all groups
103
+ kctl-telegram messages schedule --text "msg" --target-id ID --at "ISO" [--bot-id] # Schedule
104
+ kctl-telegram messages scheduled # List pending
105
+ kctl-telegram messages cancel <id> # Cancel scheduled
106
+ ```
107
+
108
+ ## Chatwoot Integration
109
+
110
+ ```bash
111
+ kctl-telegram chatwoot list # List Chatwoot inboxes
112
+ kctl-telegram chatwoot add --bot-id ID --inbox-id ID --base-url URL --api-token TK # Add inbox
113
+ kctl-telegram chatwoot remove <id> [--force] # Remove inbox mapping
114
+ ```
115
+
116
+ ## API Structure
117
+
118
+ The CLI talks to the kodemeio-telegram FastAPI service:
119
+
120
+ - **Base URL**: `https://telegram.kodeme.io/api/v1`
121
+ - **Auth**: `X-Api-Key` header
122
+ - **Resources**: bots, groups, messages, chatwoot/inboxes
123
+ - **Health**: GET /api/v1/health, GET /api/v1/ready
124
+
125
+ ## Architecture
126
+
127
+ - **FastAPI** + python-telegram-bot (multi-bot, webhook mode)
128
+ - **PostgreSQL** (shared via kodemeio-postgres-16)
129
+ - **Redis** (rate limiting, caching)
130
+ - **BotManager** orchestrates multiple bots
131
+ - **APScheduler** for scheduled messages
132
+ - **Chatwoot/Odoo** integrations via webhook relay
133
+
134
+ ## Troubleshooting
135
+
136
+ ```bash
137
+ # Check health
138
+ kctl-telegram health
139
+
140
+ # Watch health continuously
141
+ kctl-telegram health --watch
142
+
143
+ # Test connection
144
+ kctl-telegram config test
145
+
146
+ # Check bot status
147
+ kctl-telegram bots list
148
+
149
+ # JSON output for debugging
150
+ kctl-telegram --json bots list | jq .
151
+ ```
@@ -0,0 +1,5 @@
1
+ """Kodemeio Telegram CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.6.3"
@@ -0,0 +1,7 @@
1
+ """Allow running as python -m kctl_telegram."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kctl_telegram.cli import _run
6
+
7
+ _run()
@@ -0,0 +1,131 @@
1
+ """Main CLI entry point for kctl-telegram."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from kctl_telegram import __version__
11
+ from kctl_telegram.commands.bots import app as bots_app
12
+ from kctl_telegram.commands.chatwoot import app as chatwoot_app
13
+ from kctl_telegram.commands.config_cmd import app as config_app
14
+ from kctl_telegram.commands.dashboard import app as dashboard_app
15
+ from kctl_telegram.commands.groups import app as groups_app
16
+ from kctl_telegram.commands.health import app as health_app
17
+ from kctl_telegram.commands.messages import app as messages_app
18
+ from kctl_telegram.core.callbacks import AppContext
19
+ from kctl_telegram.core.exceptions import KctlError
20
+ from kctl_telegram.commands.skill_cmd import app as skill_app
21
+ from kctl_telegram.commands.doctor_cmd import app as doctor_app
22
+ from kctl_lib.self_update import notify_if_outdated
23
+ from kctl_lib.tui import add_tui_command
24
+
25
+
26
+ def version_callback(value: bool) -> None:
27
+ if value:
28
+ typer.echo(f"kctl-telegram {__version__}")
29
+ raise typer.Exit()
30
+
31
+
32
+ app = typer.Typer(
33
+ name="kctl-telegram",
34
+ help="Kodemeio Telegram CLI - manage your Telegram bot gateway.",
35
+ no_args_is_help=True,
36
+ rich_markup_mode="rich",
37
+ pretty_exceptions_enable=False,
38
+ )
39
+
40
+
41
+ @app.callback()
42
+ def main(
43
+ ctx: typer.Context,
44
+ json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
45
+ quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
46
+ format_: Annotated[str, typer.Option("--format", "-f", help="Output format: pretty/json/csv/yaml")] = "pretty",
47
+ no_header: Annotated[bool, typer.Option("--no-header", help="Suppress table headers (csv)")] = False,
48
+ profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
49
+ url: Annotated[str | None, typer.Option("--url", help="API URL override")] = None,
50
+ api_key: Annotated[str | None, typer.Option("--api-key", help="API key override")] = None,
51
+ version: Annotated[
52
+ bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
53
+ ] = False,
54
+ ) -> None:
55
+ """Kodemeio Telegram CLI."""
56
+ ctx.ensure_object(dict)
57
+ ctx.obj = AppContext(
58
+ json_mode=json_output,
59
+ quiet=quiet,
60
+ format=format_,
61
+ no_header=no_header,
62
+ profile=profile,
63
+ url_override=url,
64
+ api_key_override=api_key,
65
+ )
66
+ notify_if_outdated(ctx.obj.output, "kctl-telegram", __version__)
67
+
68
+
69
+ # Register all command groups
70
+ app.add_typer(config_app, name="config")
71
+ app.add_typer(health_app, name="health")
72
+ app.add_typer(dashboard_app, name="dashboard")
73
+ app.add_typer(bots_app, name="bots")
74
+ app.add_typer(groups_app, name="groups")
75
+ app.add_typer(messages_app, name="messages")
76
+ app.add_typer(chatwoot_app, name="chatwoot")
77
+ app.add_typer(skill_app, name="skill", hidden=True)
78
+ app.add_typer(doctor_app, name="doctor")
79
+ add_tui_command(app, service_key="telegram", version=__version__)
80
+
81
+
82
+ @app.command("self-update")
83
+ def self_update_cmd(ctx: typer.Context) -> None:
84
+ """Check for updates and upgrade kctl-telegram."""
85
+ actx = ctx.obj
86
+ out = actx.output
87
+
88
+ from kctl_lib.self_update import check_update
89
+ from kctl_lib.self_update import update as do_update
90
+
91
+ latest = check_update("kctl-telegram", __version__)
92
+ if latest:
93
+ out.info(f"Updating to {latest}...")
94
+ do_update("kctl-telegram")
95
+ out.success(f"Updated to {latest}")
96
+ else:
97
+ out.success("Already up to date")
98
+
99
+
100
+ @app.command()
101
+ def completions(
102
+ shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
103
+ install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
104
+ ) -> None:
105
+ """Generate or install shell completions."""
106
+ from kctl_lib.completions import get_completion_script, install_completions
107
+
108
+ if install:
109
+ path = install_completions("kctl-telegram", shell)
110
+ if path:
111
+ typer.echo(f"Completions installed to {path}")
112
+ else:
113
+ typer.echo(f"Could not install completions for {shell}", err=True)
114
+ raise typer.Exit(code=1)
115
+ else:
116
+ script = get_completion_script("kctl-telegram", shell)
117
+ typer.echo(script)
118
+
119
+
120
+ def _run() -> None:
121
+ """Entry point with error handling."""
122
+ try:
123
+ app()
124
+ except KctlError as e:
125
+ from kctl_lib import handle_cli_error
126
+
127
+ handle_cli_error(e)
128
+
129
+
130
+ if __name__ == "__main__":
131
+ _run()
@@ -0,0 +1 @@
1
+ """Command modules for kctl-telegram."""
@@ -0,0 +1,175 @@
1
+ """Bot management commands.
2
+
3
+ List, inspect, create, update, and remove Telegram bots.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ from kctl_telegram.core.callbacks import AppContext
13
+
14
+ app = typer.Typer(help="Manage Telegram bots.")
15
+
16
+
17
+ @app.command("list")
18
+ def list_(ctx: typer.Context) -> None:
19
+ """List all registered bots."""
20
+ actx: AppContext = ctx.obj
21
+ out = actx.output
22
+ c = actx.client
23
+
24
+ data = c.get("bots")
25
+ bots = data.get("results", []) if isinstance(data, dict) else data if isinstance(data, list) else []
26
+
27
+ if not bots:
28
+ out.warn("No bots found.")
29
+ return
30
+
31
+ rows: list[list[str]] = []
32
+ json_data: list[dict] = []
33
+
34
+ for bot in bots:
35
+ bot_id = str(bot.get("id", ""))
36
+ username = bot.get("username", "")
37
+ display_name = bot.get("display_name", "")
38
+ is_active = bot.get("is_active", False)
39
+ status = "[green]active[/green]" if is_active else "[red]inactive[/red]"
40
+
41
+ rows.append([bot_id, f"@{username}" if username else "-", display_name, status])
42
+ json_data.append(bot)
43
+
44
+ out.table(
45
+ "Bots",
46
+ [("ID", "cyan"), ("Username", ""), ("Display Name", ""), ("Status", "")],
47
+ rows,
48
+ data_for_json=json_data,
49
+ )
50
+
51
+
52
+ @app.command()
53
+ def get(
54
+ ctx: typer.Context,
55
+ bot_id: Annotated[int, typer.Argument(help="Bot ID")],
56
+ ) -> None:
57
+ """Show bot details."""
58
+ actx: AppContext = ctx.obj
59
+ out = actx.output
60
+ c = actx.client
61
+
62
+ bot = c.get(f"bots/{bot_id}")
63
+
64
+ sections: list[tuple[str, list[tuple[str, str]]]] = []
65
+
66
+ info_kvs: list[tuple[str, str]] = [
67
+ ("ID", str(bot.get("id", ""))),
68
+ ("Username", f"@{bot.get('username', '')}" if bot.get("username") else "-"),
69
+ ("Display Name", bot.get("display_name", "")),
70
+ ("Is Active", "[green]Yes[/green]" if bot.get("is_active") else "[red]No[/red]"),
71
+ ]
72
+
73
+ # Show token masked
74
+ token = bot.get("token", "")
75
+ if token:
76
+ masked = f"{token[:4]}{'*' * (len(token) - 8)}{token[-4:]}" if len(token) > 10 else "****"
77
+ info_kvs.append(("Token", masked))
78
+
79
+ sections.append(("Bot Info", info_kvs))
80
+
81
+ # Timestamps
82
+ ts_kvs: list[tuple[str, str]] = []
83
+ if bot.get("created_at"):
84
+ ts_kvs.append(("Created", bot["created_at"]))
85
+ if bot.get("updated_at"):
86
+ ts_kvs.append(("Updated", bot["updated_at"]))
87
+ if ts_kvs:
88
+ sections.append(("Timestamps", ts_kvs))
89
+
90
+ out.detail(f"Bot: {bot.get('display_name', bot.get('username', bot_id))}", sections, data_for_json=bot)
91
+
92
+
93
+ @app.command()
94
+ def add(
95
+ ctx: typer.Context,
96
+ token: Annotated[str, typer.Option("--token", help="Telegram bot token from BotFather.")],
97
+ display_name: Annotated[str | None, typer.Option("--display-name", help="Friendly display name.")] = None,
98
+ ) -> None:
99
+ """Register a new Telegram bot."""
100
+ actx: AppContext = ctx.obj
101
+ out = actx.output
102
+ c = actx.client
103
+
104
+ payload: dict = {"token": token}
105
+ if display_name:
106
+ payload["display_name"] = display_name
107
+
108
+ bot = c.post("bots", json=payload)
109
+
110
+ out.success(f"Bot created: {bot.get('display_name', bot.get('username', ''))}")
111
+ out.kv("ID", str(bot.get("id", "")))
112
+ out.kv("Username", f"@{bot.get('username', '')}" if bot.get("username") else "-")
113
+ out.kv("Display Name", bot.get("display_name", ""))
114
+ out.kv("Active", str(bot.get("is_active", True)))
115
+
116
+ if actx.output.json_mode:
117
+ out.raw_json(bot)
118
+
119
+
120
+ @app.command()
121
+ def update(
122
+ ctx: typer.Context,
123
+ bot_id: Annotated[int, typer.Argument(help="Bot ID")],
124
+ display_name: Annotated[str | None, typer.Option("--display-name", help="New display name.")] = None,
125
+ is_active: Annotated[bool | None, typer.Option("--is-active/--no-active", help="Enable or disable bot.")] = None,
126
+ ) -> None:
127
+ """Update a bot's settings."""
128
+ actx: AppContext = ctx.obj
129
+ out = actx.output
130
+ c = actx.client
131
+
132
+ payload: dict = {}
133
+ if display_name is not None:
134
+ payload["display_name"] = display_name
135
+ if is_active is not None:
136
+ payload["is_active"] = is_active
137
+
138
+ if not payload:
139
+ out.warn("No changes specified. Use --display-name or --is-active/--no-active.")
140
+ raise typer.Exit(1)
141
+
142
+ bot = c.patch(f"bots/{bot_id}", json=payload)
143
+
144
+ out.success(f"Bot {bot_id} updated")
145
+ out.kv("Display Name", bot.get("display_name", ""))
146
+ out.kv("Active", str(bot.get("is_active", "")))
147
+
148
+ if actx.output.json_mode:
149
+ out.raw_json(bot)
150
+
151
+
152
+ @app.command()
153
+ def remove(
154
+ ctx: typer.Context,
155
+ bot_id: Annotated[int, typer.Argument(help="Bot ID")],
156
+ force: Annotated[bool, typer.Option("--force", help="Skip confirmation")] = False,
157
+ ) -> None:
158
+ """Remove a bot."""
159
+ actx: AppContext = ctx.obj
160
+ out = actx.output
161
+ c = actx.client
162
+
163
+ if not force:
164
+ # Fetch bot info for confirmation
165
+ try:
166
+ bot = c.get(f"bots/{bot_id}")
167
+ name = bot.get("display_name", bot.get("username", str(bot_id)))
168
+ except Exception:
169
+ name = str(bot_id)
170
+
171
+ if not typer.confirm(f"Remove bot '{name}' (ID: {bot_id})?"):
172
+ raise typer.Exit(0)
173
+
174
+ c.delete(f"bots/{bot_id}")
175
+ out.success(f"Bot {bot_id} removed")