ensue-cli 0.1.0__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.
ensue_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Ensue CLI - Command line interface for the Ensue Memory Network."""
2
+
3
+ __version__ = "0.1.0"
ensue_cli/cli.py ADDED
@@ -0,0 +1,141 @@
1
+ """Dynamic CLI for Ensue Memory Network."""
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import sys
7
+
8
+ import click
9
+ from rich.console import Console
10
+ from rich.json import JSON
11
+
12
+ from . import client
13
+
14
+ console = Console()
15
+ DEFAULT_URL = "https://www.ensue-network.ai/api/"
16
+
17
+
18
+ def run_async(coro):
19
+ """Run async coroutine, handling nested event loops."""
20
+ try:
21
+ asyncio.get_running_loop()
22
+ except RuntimeError:
23
+ return asyncio.run(coro)
24
+ # If there's already a running loop, create a new one in a thread
25
+ import concurrent.futures
26
+
27
+ with concurrent.futures.ThreadPoolExecutor() as pool:
28
+ return pool.submit(asyncio.run, coro).result()
29
+
30
+
31
+ def get_config():
32
+ url = os.environ.get("ENSUE_URL", DEFAULT_URL)
33
+ token = os.environ.get("ENSUE_TOKEN")
34
+ if not token:
35
+ console.print("[red]Error:[/red] ENSUE_TOKEN environment variable required")
36
+ sys.exit(1)
37
+ return url, token
38
+
39
+
40
+ def print_result(result):
41
+ if hasattr(result, "content"):
42
+ for item in result.content:
43
+ if hasattr(item, "text"):
44
+ try:
45
+ console.print(JSON(item.text))
46
+ except Exception:
47
+ console.print(item.text)
48
+ else:
49
+ console.print(JSON(json.dumps(result, indent=2)))
50
+
51
+
52
+ TYPE_MAP = {
53
+ "integer": click.INT,
54
+ "number": click.FLOAT,
55
+ "boolean": click.BOOL,
56
+ }
57
+
58
+
59
+ def parse_arg(value, schema_type):
60
+ if schema_type in ("array", "object") and isinstance(value, str):
61
+ try:
62
+ return json.loads(value)
63
+ except json.JSONDecodeError:
64
+ pass
65
+ return value
66
+
67
+
68
+ def build_command(tool):
69
+ """Build a Click command from an MCP tool definition."""
70
+ schema = tool.get("inputSchema", {})
71
+ props = schema.get("properties", {})
72
+ required = set(schema.get("required", []))
73
+
74
+ params = [
75
+ click.Option(
76
+ [f"--{name.replace('_', '-')}"],
77
+ type=TYPE_MAP.get(p.get("type"), click.STRING),
78
+ required=name in required,
79
+ help=p.get("description", ""),
80
+ )
81
+ for name, p in props.items()
82
+ ]
83
+
84
+ def callback(**kwargs):
85
+ url, token = get_config()
86
+ args = {
87
+ k.replace("-", "_"): parse_arg(v, props.get(k.replace("-", "_"), {}).get("type"))
88
+ for k, v in kwargs.items()
89
+ if v is not None
90
+ }
91
+ result = run_async(client.call_tool(url, token, tool["name"], args))
92
+ print_result(result)
93
+
94
+ return click.Command(
95
+ name=tool["name"],
96
+ callback=callback,
97
+ params=params,
98
+ help=tool.get("description", ""),
99
+ )
100
+
101
+
102
+ class MCPToolsCLI(click.Group):
103
+ """CLI that loads commands dynamically from MCP server."""
104
+
105
+ def __init__(self, **kwargs):
106
+ super().__init__(**kwargs)
107
+ self._tools = None
108
+
109
+ @property
110
+ def tools(self):
111
+ if self._tools is None:
112
+ url, token = get_config()
113
+ self._tools = {t["name"]: t for t in run_async(client.list_tools(url, token))}
114
+ return self._tools
115
+
116
+ def list_commands(self, ctx):
117
+ try:
118
+ return sorted(self.tools.keys())
119
+ except Exception as e:
120
+ console.print("[red]Connection error:[/red] Could not connect to MCP server")
121
+ console.print(f"[dim]{e}[/dim]")
122
+ return []
123
+
124
+ def get_command(self, ctx, name):
125
+ if name not in self.tools:
126
+ return None
127
+ return build_command(self.tools[name])
128
+
129
+
130
+ @click.group(cls=MCPToolsCLI)
131
+ @click.version_option()
132
+ def main():
133
+ """Ensue Memory CLI - A distributed memory network for AI agents.
134
+
135
+ Commands are loaded dynamically from the MCP server.
136
+ Set ENSUE_TOKEN to authenticate.
137
+ """
138
+
139
+
140
+ if __name__ == "__main__":
141
+ main()
ensue_cli/client.py ADDED
@@ -0,0 +1,38 @@
1
+ """MCP client for communicating with the Ensue Memory Network."""
2
+
3
+ from contextlib import asynccontextmanager
4
+ from typing import Any
5
+
6
+ from mcp import ClientSession
7
+ from mcp.client.streamable_http import streamablehttp_client
8
+
9
+
10
+ @asynccontextmanager
11
+ async def create_session(url: str, token: str):
12
+ """Create an MCP client session connected to the Ensue service."""
13
+ headers = {"Authorization": f"Bearer {token}"}
14
+ async with streamablehttp_client(url, headers=headers) as (read_stream, write_stream, _):
15
+ async with ClientSession(read_stream, write_stream) as session:
16
+ await session.initialize()
17
+ yield session
18
+
19
+
20
+ async def list_tools(url: str, token: str) -> list[dict[str, Any]]:
21
+ """Fetch the list of available tools from the MCP server."""
22
+ async with create_session(url, token) as session:
23
+ result = await session.list_tools()
24
+ return [
25
+ {
26
+ "name": tool.name,
27
+ "description": tool.description,
28
+ "inputSchema": tool.inputSchema,
29
+ }
30
+ for tool in result.tools
31
+ ]
32
+
33
+
34
+ async def call_tool(url: str, token: str, name: str, arguments: dict[str, Any]) -> Any:
35
+ """Call a tool on the MCP server."""
36
+ async with create_session(url, token) as session:
37
+ result = await session.call_tool(name, arguments)
38
+ return result
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: ensue-cli
3
+ Version: 0.1.0
4
+ Summary: CLI for Ensue Memory - a distributed memory network for AI agents built on MCP, enabling persistent, shared context across conversations and applications
5
+ Project-URL: Homepage, https://www.ensue-network.ai
6
+ Project-URL: Repository, https://github.com/mutable-state-inc/ensue-cli
7
+ Project-URL: Documentation, https://www.ensue-network.ai/docs
8
+ Author: Mutable State Inc
9
+ Keywords: agents,ai,cli,ensue,mcp,memory,model-context-protocol
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: click>=8.0
22
+ Requires-Dist: mcp>=1.0
23
+ Requires-Dist: rich>=13.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: build>=1.0; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
27
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
28
+ Requires-Dist: pytest>=7.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.8; extra == 'dev'
30
+ Requires-Dist: twine>=4.0; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # ensue-cli
34
+
35
+ CLI for Ensue Memory - a distributed memory network for AI agents built on MCP, enabling persistent, shared context across conversations and applications.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install ensue-cli
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ Set your authentication token:
46
+
47
+ ```bash
48
+ export ENSUE_TOKEN=your-token
49
+ ```
50
+
51
+ List available commands:
52
+
53
+ ```bash
54
+ ensue --help
55
+ ```
56
+
57
+ Commands are loaded dynamically from the MCP server.
58
+
59
+ ## Configuration
60
+
61
+ - `ENSUE_TOKEN` (required): Your Ensue API token
62
+ - `ENSUE_URL` (optional): API endpoint (defaults to https://www.ensue-network.ai/api/)
@@ -0,0 +1,7 @@
1
+ ensue_cli/__init__.py,sha256=eBr2AZuS3u6UQ__lTRZUZQ3uKXus_x0Q6Mzo-fdBgtI,94
2
+ ensue_cli/cli.py,sha256=UIqJIB0cXI7rsKMou0bFuy9FykivqcqFajlSGCvoiQc,3728
3
+ ensue_cli/client.py,sha256=wSDs13wNEAalV47ABCH-NgF2kszz0CJ91A2NZg49ab0,1372
4
+ ensue_cli-0.1.0.dist-info/METADATA,sha256=SZjcHqc3QLRhvh4pD_w64241PAAw19c6VerSOgAJ1vI,1978
5
+ ensue_cli-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ ensue_cli-0.1.0.dist-info/entry_points.txt,sha256=VrEgSstWbWzfCsfWjGqaTDk3xTaRsZ1Ihrt_heC-UDQ,45
7
+ ensue_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ensue = ensue_cli.cli:main