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 +3 -0
- ensue_cli/cli.py +141 -0
- ensue_cli/client.py +38 -0
- ensue_cli-0.1.0.dist-info/METADATA +62 -0
- ensue_cli-0.1.0.dist-info/RECORD +7 -0
- ensue_cli-0.1.0.dist-info/WHEEL +4 -0
- ensue_cli-0.1.0.dist-info/entry_points.txt +2 -0
ensue_cli/__init__.py
ADDED
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,,
|