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.
- cgate/__init__.py +26 -0
- cgate/__main__.py +8 -0
- cgate/_version.py +24 -0
- cgate/cli/__init__.py +1 -0
- cgate/cli/_console.py +17 -0
- cgate/cli/connections.py +212 -0
- cgate/cli/history.py +191 -0
- cgate/cli/install.py +182 -0
- cgate/cli/main.py +115 -0
- cgate/cli/mcp.py +197 -0
- cgate/cli/uninstall.py +403 -0
- cgate/cli/update.py +538 -0
- cgate/cli/watch.py +20 -0
- cgate/connections/__init__.py +1 -0
- cgate/connections/auth.py +93 -0
- cgate/connections/detect.py +78 -0
- cgate/connections/store.py +88 -0
- cgate/core/__init__.py +1 -0
- cgate/core/path_env.py +218 -0
- cgate/core/paths.py +35 -0
- cgate/core/update_log.py +36 -0
- cgate/db/__init__.py +1 -0
- cgate/db/batches.py +111 -0
- cgate/db/commands.py +191 -0
- cgate/db/connection.py +104 -0
- cgate/db/mode.py +74 -0
- cgate/db/rows.py +99 -0
- cgate/db/schema.py +54 -0
- cgate/db/server_settings.py +105 -0
- cgate/db/types.py +77 -0
- cgate/executor/__init__.py +7 -0
- cgate/executor/base.py +71 -0
- cgate/executor/selector.py +61 -0
- cgate/executor/ssh.py +157 -0
- cgate/executor/winrm.py +129 -0
- cgate/helper/__init__.py +10 -0
- cgate/helper/__main__.py +112 -0
- cgate/helper/waiter.py +123 -0
- cgate/mcp_installer.py +161 -0
- cgate/mcp_server/__init__.py +6 -0
- cgate/mcp_server/__main__.py +6 -0
- cgate/mcp_server/auto_resolution.py +80 -0
- cgate/mcp_server/server.py +271 -0
- cgate/mcp_server/tools.py +351 -0
- cgate/risk.py +129 -0
- cgate/update.py +713 -0
- cgate/watch/__init__.py +7 -0
- cgate/watch/app.py +560 -0
- cgate/watch/approval.py +237 -0
- cgate/watch/command_detail_modal.py +68 -0
- cgate/watch/history_modal.py +242 -0
- cgate/watch/mode_modal.py +110 -0
- cgate/watch/queue.py +106 -0
- cgate/watch/render.py +156 -0
- cgate/watch/server_settings_modal.py +179 -0
- cgate/watch/session.py +40 -0
- cgate/watch/theme.py +32 -0
- cgate/watch/widgets.py +35 -0
- command_gate-0.2.4.dist-info/METADATA +204 -0
- command_gate-0.2.4.dist-info/RECORD +63 -0
- command_gate-0.2.4.dist-info/WHEEL +4 -0
- command_gate-0.2.4.dist-info/entry_points.txt +2 -0
- 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()
|