kctl-telegram 0.6.3__py3-none-any.whl

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.
@@ -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()
kctl_telegram/cli.py ADDED
@@ -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")
@@ -0,0 +1,105 @@
1
+ """Chatwoot integration commands.
2
+
3
+ Manage Chatwoot inbox connections for 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 Chatwoot integrations.")
15
+
16
+
17
+ @app.command("list")
18
+ def list_(ctx: typer.Context) -> None:
19
+ """List Chatwoot inboxes linked to Telegram bots."""
20
+ actx: AppContext = ctx.obj
21
+ out = actx.output
22
+ c = actx.client
23
+
24
+ data = c.get("chatwoot/inboxes")
25
+ inboxes = data.get("results", []) if isinstance(data, dict) else data if isinstance(data, list) else []
26
+
27
+ if not inboxes:
28
+ out.warn("No Chatwoot inboxes found.")
29
+ return
30
+
31
+ rows: list[list[str]] = []
32
+ json_data: list[dict] = []
33
+
34
+ for inbox in inboxes:
35
+ inbox_id = str(inbox.get("id", ""))
36
+ bot_id = str(inbox.get("bot_id", ""))
37
+ identifier = inbox.get("inbox_identifier", "")
38
+ base_url = inbox.get("base_url", "")
39
+
40
+ rows.append([inbox_id, bot_id, identifier, base_url])
41
+ json_data.append(inbox)
42
+
43
+ out.table(
44
+ "Chatwoot Inboxes",
45
+ [("ID", "cyan"), ("Bot ID", ""), ("Inbox Identifier", ""), ("Chatwoot URL", "")],
46
+ rows,
47
+ data_for_json=json_data,
48
+ )
49
+
50
+
51
+ @app.command()
52
+ def add(
53
+ ctx: typer.Context,
54
+ bot_id: Annotated[int, typer.Option("--bot-id", help="Telegram bot ID.")],
55
+ inbox_identifier: Annotated[str, typer.Option("--inbox-identifier", help="Chatwoot inbox identifier.")],
56
+ base_url: Annotated[str, typer.Option("--base-url", help="Chatwoot instance base URL.")],
57
+ api_token: Annotated[str, typer.Option("--api-token", help="Chatwoot API token.")],
58
+ ) -> None:
59
+ """Link a Chatwoot inbox to a Telegram bot."""
60
+ actx: AppContext = ctx.obj
61
+ out = actx.output
62
+ c = actx.client
63
+
64
+ payload: dict = {
65
+ "bot_id": bot_id,
66
+ "inbox_identifier": inbox_identifier,
67
+ "base_url": base_url,
68
+ "api_token": api_token,
69
+ }
70
+
71
+ result = c.post("chatwoot/inboxes", json=payload)
72
+
73
+ out.success("Chatwoot inbox linked")
74
+ out.kv("ID", str(result.get("id", "")))
75
+ out.kv("Bot ID", str(bot_id))
76
+ out.kv("Inbox", inbox_identifier)
77
+ out.kv("Chatwoot URL", base_url)
78
+
79
+ if actx.output.json_mode:
80
+ out.raw_json(result)
81
+
82
+
83
+ @app.command()
84
+ def remove(
85
+ ctx: typer.Context,
86
+ inbox_id: Annotated[int, typer.Argument(help="Chatwoot inbox ID to remove")],
87
+ force: Annotated[bool, typer.Option("--force", help="Skip confirmation")] = False,
88
+ ) -> None:
89
+ """Remove a Chatwoot inbox integration."""
90
+ actx: AppContext = ctx.obj
91
+ out = actx.output
92
+ c = actx.client
93
+
94
+ if not force:
95
+ try:
96
+ inbox = c.get(f"chatwoot/inboxes/{inbox_id}")
97
+ identifier = inbox.get("inbox_identifier", str(inbox_id))
98
+ except Exception:
99
+ identifier = str(inbox_id)
100
+
101
+ if not typer.confirm(f"Remove Chatwoot inbox '{identifier}' (ID: {inbox_id})?"):
102
+ raise typer.Exit(0)
103
+
104
+ c.delete(f"chatwoot/inboxes/{inbox_id}")
105
+ out.success(f"Chatwoot inbox {inbox_id} removed")