command-gate 0.2.4__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.
Files changed (63) hide show
  1. cgate/__init__.py +26 -0
  2. cgate/__main__.py +8 -0
  3. cgate/_version.py +24 -0
  4. cgate/cli/__init__.py +1 -0
  5. cgate/cli/_console.py +17 -0
  6. cgate/cli/connections.py +212 -0
  7. cgate/cli/history.py +191 -0
  8. cgate/cli/install.py +182 -0
  9. cgate/cli/main.py +115 -0
  10. cgate/cli/mcp.py +197 -0
  11. cgate/cli/uninstall.py +403 -0
  12. cgate/cli/update.py +538 -0
  13. cgate/cli/watch.py +20 -0
  14. cgate/connections/__init__.py +1 -0
  15. cgate/connections/auth.py +93 -0
  16. cgate/connections/detect.py +78 -0
  17. cgate/connections/store.py +88 -0
  18. cgate/core/__init__.py +1 -0
  19. cgate/core/path_env.py +218 -0
  20. cgate/core/paths.py +35 -0
  21. cgate/core/update_log.py +36 -0
  22. cgate/db/__init__.py +1 -0
  23. cgate/db/batches.py +111 -0
  24. cgate/db/commands.py +191 -0
  25. cgate/db/connection.py +104 -0
  26. cgate/db/mode.py +74 -0
  27. cgate/db/rows.py +99 -0
  28. cgate/db/schema.py +54 -0
  29. cgate/db/server_settings.py +105 -0
  30. cgate/db/types.py +77 -0
  31. cgate/executor/__init__.py +7 -0
  32. cgate/executor/base.py +71 -0
  33. cgate/executor/selector.py +61 -0
  34. cgate/executor/ssh.py +157 -0
  35. cgate/executor/winrm.py +129 -0
  36. cgate/helper/__init__.py +10 -0
  37. cgate/helper/__main__.py +112 -0
  38. cgate/helper/waiter.py +123 -0
  39. cgate/mcp_installer.py +161 -0
  40. cgate/mcp_server/__init__.py +6 -0
  41. cgate/mcp_server/__main__.py +6 -0
  42. cgate/mcp_server/auto_resolution.py +80 -0
  43. cgate/mcp_server/server.py +271 -0
  44. cgate/mcp_server/tools.py +351 -0
  45. cgate/risk.py +129 -0
  46. cgate/update.py +713 -0
  47. cgate/watch/__init__.py +7 -0
  48. cgate/watch/app.py +560 -0
  49. cgate/watch/approval.py +237 -0
  50. cgate/watch/command_detail_modal.py +68 -0
  51. cgate/watch/history_modal.py +242 -0
  52. cgate/watch/mode_modal.py +110 -0
  53. cgate/watch/queue.py +106 -0
  54. cgate/watch/render.py +156 -0
  55. cgate/watch/server_settings_modal.py +179 -0
  56. cgate/watch/session.py +40 -0
  57. cgate/watch/theme.py +32 -0
  58. cgate/watch/widgets.py +35 -0
  59. command_gate-0.2.4.dist-info/METADATA +204 -0
  60. command_gate-0.2.4.dist-info/RECORD +63 -0
  61. command_gate-0.2.4.dist-info/WHEEL +4 -0
  62. command_gate-0.2.4.dist-info/entry_points.txt +2 -0
  63. command_gate-0.2.4.dist-info/licenses/LICENSE +21 -0
cgate/cli/main.py ADDED
@@ -0,0 +1,115 @@
1
+ """Top-level command-gate CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from cgate import __version__
12
+ from cgate.cli.connections import connections_app
13
+ from cgate.cli.history import history_app
14
+ from cgate.cli.install import install_cmd, maybe_auto_install
15
+ from cgate.cli.mcp import mcp_app
16
+ from cgate.cli.uninstall import uninstall_cmd
17
+ from cgate.cli.update import update_app
18
+ from cgate.cli.watch import watch_app
19
+ from cgate.update import maybe_heal_pending_update
20
+
21
+ app = typer.Typer(
22
+ name="cgate",
23
+ help="Middleware/CLI between AI agents and servers -- IA proposes, human approves.",
24
+ invoke_without_command=True,
25
+ )
26
+ app.add_typer(connections_app, name="connections")
27
+ app.add_typer(history_app, name="history")
28
+ app.add_typer(mcp_app, name="mcp")
29
+ app.add_typer(update_app, name="update")
30
+ app.add_typer(watch_app, name="watch")
31
+ app.command("install")(install_cmd)
32
+ app.command("uninstall")(uninstall_cmd)
33
+
34
+
35
+ def _version_callback(*, value: bool) -> None:
36
+ if value:
37
+ typer.echo(f"cgate {__version__}")
38
+ raise typer.Exit(code=0)
39
+
40
+
41
+ @app.callback()
42
+ def root(
43
+ ctx: typer.Context,
44
+ *,
45
+ _version: Annotated[
46
+ bool,
47
+ typer.Option(
48
+ "--version",
49
+ "-V",
50
+ callback=_version_callback,
51
+ is_eager=True,
52
+ help="Show version and exit.",
53
+ ),
54
+ ] = False,
55
+ unattended: Annotated[
56
+ bool,
57
+ typer.Option(
58
+ "--unattended",
59
+ help=(
60
+ "For first-run auto-install only: skip the 'Press Enter to "
61
+ "close' pause and exit immediately once done. No prompts "
62
+ "happen either way -- this only controls the pause, for "
63
+ "scripted/silent deployment."
64
+ ),
65
+ ),
66
+ ] = False,
67
+ ) -> None:
68
+ """Display help or route to a command group.
69
+
70
+ A completely bare invocation (no subcommand, no options) used to
71
+ just print help via Typer's ``no_args_is_help``. Now it first checks
72
+ whether this is a freshly downloaded binary that isn't installed
73
+ yet -- if so, ``maybe_auto_install()`` runs the full first-run setup
74
+ instead, so "download and double-click" is enough on its own. Once
75
+ properly installed, bare `cgate` goes back to printing help, exactly
76
+ as before.
77
+ """
78
+ if ctx.invoked_subcommand is not None:
79
+ return
80
+ if maybe_auto_install(unattended=unattended):
81
+ return
82
+ typer.echo(ctx.get_help())
83
+
84
+
85
+ def main() -> None:
86
+ """Run the CLI, turning an uncaught sqlite3.Error into a clean exit.
87
+
88
+ Every command opens its own SQLite connection with no shared boundary
89
+ that catches DB failures; a locked database (another cgate process
90
+ holding it) or a corrupted file otherwise surfaces as a raw traceback
91
+ from deep inside whichever command hit it first, instead of a clear
92
+ message (issue #12). This is the entry point PyInstaller's bundled
93
+ binary and the pip console-script both call, so it covers every
94
+ command without needing its own try/except.
95
+
96
+ Before dispatching the command, attempt to self-heal a staged update
97
+ left behind by a previous failed ``update apply``: if ``<binary>.new``
98
+ is on disk and no other ``cgate.exe`` is alive, we spawn the helper
99
+ (detached, waiting on our PID) so the swap completes the moment we
100
+ exit. Fire-and-forget, silent on any condition that prevents it.
101
+ """
102
+ maybe_heal_pending_update()
103
+ try:
104
+ app()
105
+ except sqlite3.Error as exc:
106
+ Console(stderr=True).print(
107
+ f"[red]cgate's local database is locked or corrupted:[/red] {exc}\n"
108
+ "[dim]Another cgate process may be holding it, or the file may "
109
+ "be damaged.[/dim]"
110
+ )
111
+ raise SystemExit(1) from exc
112
+
113
+
114
+ if __name__ == "__main__":
115
+ main()
cgate/cli/mcp.py ADDED
@@ -0,0 +1,197 @@
1
+ """Manage cgate MCP server registration and stdio serving."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.rule import Rule
10
+ from rich.table import Table
11
+
12
+ from cgate.cli._console import should_pause, wait_for_enter
13
+ from cgate.mcp_installer import (
14
+ CLIENTS,
15
+ current_binary_command,
16
+ detect_clients,
17
+ is_registered,
18
+ register,
19
+ unregister,
20
+ )
21
+
22
+ mcp_app = typer.Typer(
23
+ help="Manage the MCP server: register with IA clients, run as server."
24
+ )
25
+ console = Console()
26
+
27
+
28
+ @mcp_app.command("install")
29
+ def install_cmd(
30
+ *,
31
+ yes: Annotated[
32
+ bool,
33
+ typer.Option(
34
+ "--yes", "-y", help="Skip Y/N confirmation per detected client."
35
+ ),
36
+ ] = False,
37
+ dry_run: Annotated[
38
+ bool,
39
+ typer.Option(
40
+ "--dry-run",
41
+ help="Show what would be done without modifying any config files.",
42
+ ),
43
+ ] = False,
44
+ no_pause: Annotated[
45
+ bool,
46
+ typer.Option(
47
+ "--no-pause",
48
+ help=(
49
+ "Skip the 'Press Enter to close' wait at the end. "
50
+ "Combine with --yes for a fully unattended install."
51
+ ),
52
+ ),
53
+ ] = False,
54
+ ) -> None:
55
+ """Detect IA clients and register cgate's MCP server with each."""
56
+ clients = detect_clients()
57
+ if not clients:
58
+ console.print("[yellow]No IA clients detected.[/yellow]")
59
+ console.print("Expected config paths under your home:")
60
+ all_paths = (path for spec in CLIENTS.values() for path in spec.config_paths)
61
+ for relative in all_paths:
62
+ console.print(f" - [dim]~/{relative}[/dim]")
63
+ return
64
+
65
+ console.print(f"Detected {len(clients)} IA client(s):")
66
+ for client in clients:
67
+ status = (
68
+ "[green]registered[/green]"
69
+ if is_registered(client)
70
+ else "[red]not registered[/red]"
71
+ )
72
+ console.print(
73
+ f" - {client.label} [dim]({client.config_path})[/dim] -- {status}"
74
+ )
75
+
76
+ command, args = current_binary_command()
77
+ console.print(
78
+ f"\ncgate command to register: [bold]{command}[/bold] {' '.join(args)}"
79
+ )
80
+
81
+ if dry_run:
82
+ console.print()
83
+ console.print(Rule("[yellow]DRY RUN -- no changes will be made[/yellow]"))
84
+ for client in clients:
85
+ action = "Re-register" if is_registered(client) else "Register"
86
+ console.print(f" Would {action} {client.label} -> {client.config_path}")
87
+ console.print(
88
+ f" With command: [bold]{command}[/bold] {' '.join(args)}"
89
+ )
90
+ return
91
+
92
+ succeeded: list[str] = []
93
+ skipped: list[str] = []
94
+ failed: list[tuple[str, str]] = []
95
+ for client in clients:
96
+ registered = is_registered(client)
97
+ prompt = (
98
+ f"Re-register {client.label}? (updates existing entry)"
99
+ if registered
100
+ else f"Register {client.label}?"
101
+ )
102
+ if not yes and not typer.confirm(prompt, default=not registered):
103
+ skipped.append(client.label)
104
+ continue
105
+ try:
106
+ register(client, command, args)
107
+ except OSError as exc:
108
+ failed.append((client.label, str(exc)))
109
+ console.print(f"[red]Failed to write {client.config_path}: {exc}[/red]")
110
+ continue
111
+ succeeded.append(client.label)
112
+ console.print(
113
+ f"[green]Registered {client.label}.[/green] "
114
+ "Restart your client to load the new MCP server."
115
+ )
116
+
117
+ console.print()
118
+ console.print(Rule("Summary"))
119
+ console.print(f" [green]Registered: {len(succeeded)}[/green]")
120
+ if skipped:
121
+ console.print(
122
+ f" [yellow]Skipped: {len(skipped)}[/yellow] ({', '.join(skipped)})"
123
+ )
124
+ if failed:
125
+ console.print(
126
+ f" [red]Failed: {len(failed)}[/red] "
127
+ f"({', '.join(label for label, _ in failed)})"
128
+ )
129
+
130
+ if should_pause(no_pause):
131
+ console.print()
132
+ wait_for_enter()
133
+
134
+ if failed:
135
+ raise typer.Exit(code=1)
136
+
137
+
138
+ @mcp_app.command("uninstall")
139
+ def uninstall_cmd(
140
+ *,
141
+ yes: Annotated[
142
+ bool, typer.Option("--yes", "-y", help="Skip confirmation.")
143
+ ] = False,
144
+ ) -> None:
145
+ """Remove cgate from all detected IA client configs."""
146
+ clients = [client for client in detect_clients() if is_registered(client)]
147
+ if not clients:
148
+ console.print(
149
+ "[dim]cgate is not registered with any detected IA client.[/dim]"
150
+ )
151
+ return
152
+ write_failed = False
153
+ for client in clients:
154
+ if not yes and not typer.confirm(
155
+ f"Unregister from {client.label}?", default=True
156
+ ):
157
+ continue
158
+ try:
159
+ _ = unregister(client)
160
+ except OSError as exc:
161
+ write_failed = True
162
+ console.print(f"[red]Failed to write {client.config_path}: {exc}[/red]")
163
+ continue
164
+ console.print(f"[green]Unregistered {client.label}.[/green]")
165
+ if write_failed:
166
+ raise typer.Exit(code=1)
167
+
168
+
169
+ @mcp_app.command("status")
170
+ def status_cmd() -> None:
171
+ """Show which detected IA clients have cgate registered."""
172
+ clients = detect_clients()
173
+ if not clients:
174
+ console.print(
175
+ "[dim]No IA clients detected (no Claude Code/opencode/Cursor config files found).[/dim]"
176
+ )
177
+ return
178
+ table = Table(title="cgate MCP registration status")
179
+ table.add_column("Client", style="bold")
180
+ table.add_column("Config path")
181
+ table.add_column("Status")
182
+ for client in clients:
183
+ status = (
184
+ "[green][OK] registered[/green]"
185
+ if is_registered(client)
186
+ else "[dim]not registered[/dim]"
187
+ )
188
+ table.add_row(client.label, str(client.config_path), status)
189
+ console.print(table)
190
+
191
+
192
+ @mcp_app.command("serve")
193
+ def serve_cmd() -> None:
194
+ """Run the MCP server on standard input and output for IA clients."""
195
+ from cgate.mcp_server.server import main as serve_main # noqa: PLC0415
196
+
197
+ serve_main()