venn-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.
- venn_cli/__init__.py +0 -0
- venn_cli/cli.py +265 -0
- venn_cli/client.py +165 -0
- venn_cli/output.py +167 -0
- venn_cli-0.1.0.dist-info/METADATA +27 -0
- venn_cli-0.1.0.dist-info/RECORD +9 -0
- venn_cli-0.1.0.dist-info/WHEEL +4 -0
- venn_cli-0.1.0.dist-info/entry_points.txt +2 -0
- venn_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
venn_cli/__init__.py
ADDED
|
File without changes
|
venn_cli/cli.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
from venn_cli.client import VennClient
|
|
12
|
+
from venn_cli import output
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_env() -> None:
|
|
16
|
+
for p in [Path.cwd() / ".env", Path(__file__).resolve().parents[2] / ".env"]:
|
|
17
|
+
if p.exists():
|
|
18
|
+
load_dotenv(p)
|
|
19
|
+
return
|
|
20
|
+
load_dotenv()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _client(ctx: click.Context) -> VennClient:
|
|
24
|
+
return ctx.obj["client"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@click.group()
|
|
28
|
+
@click.option("--api-key", envvar="VENN_API_KEY", help="Venn API key (or set VENN_API_KEY)")
|
|
29
|
+
@click.option("--base-url", envvar="VENN_BASE_URL", default=None, help="Override API base URL")
|
|
30
|
+
@click.option("--raw", is_flag=True, help="Output raw JSON instead of formatted tables")
|
|
31
|
+
@click.version_option(package_name="venn-cli")
|
|
32
|
+
@click.pass_context
|
|
33
|
+
def main(ctx: click.Context, api_key: str | None, base_url: str | None, raw: bool) -> None:
|
|
34
|
+
"""Venn ToolIQ CLI — discover, inspect, and execute tools."""
|
|
35
|
+
_load_env()
|
|
36
|
+
api_key = api_key or os.environ.get("VENN_API_KEY")
|
|
37
|
+
if not api_key:
|
|
38
|
+
click.echo("Error: VENN_API_KEY not set. Pass --api-key or add it to .env", err=True)
|
|
39
|
+
raise SystemExit(1)
|
|
40
|
+
kwargs = {"api_key": api_key}
|
|
41
|
+
if base_url:
|
|
42
|
+
kwargs["base_url"] = base_url
|
|
43
|
+
ctx.ensure_object(dict)
|
|
44
|
+
ctx.obj["client"] = VennClient(**kwargs)
|
|
45
|
+
ctx.obj["raw"] = raw
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# venn tools
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
@main.group()
|
|
53
|
+
def tools():
|
|
54
|
+
"""Discover, inspect, and execute tools."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@tools.command("list")
|
|
58
|
+
@click.option("--slug", "-s", help="Toolset slug (e.g. 'salesforce', 'gmail')")
|
|
59
|
+
@click.option("--directory-id", "-d", help="Server directory UUID")
|
|
60
|
+
@click.option("--instance-id", "-i", help="Server instance UUID")
|
|
61
|
+
@click.pass_context
|
|
62
|
+
def tools_list(ctx, slug, directory_id, instance_id):
|
|
63
|
+
"""List tools for a server."""
|
|
64
|
+
if not any([slug, directory_id, instance_id]):
|
|
65
|
+
click.echo("Provide at least one of: --slug, --directory-id, --instance-id", err=True)
|
|
66
|
+
raise SystemExit(1)
|
|
67
|
+
data = _client(ctx).list_tools(slug=slug, directory_id=directory_id, instance_id=instance_id)
|
|
68
|
+
if ctx.obj["raw"]:
|
|
69
|
+
output.print_json(data)
|
|
70
|
+
else:
|
|
71
|
+
output.print_tool_list(data.get("result", {}))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@tools.command("search")
|
|
75
|
+
@click.argument("query")
|
|
76
|
+
@click.option("--limit", "-n", default=10, type=int, help="Max results")
|
|
77
|
+
@click.option("--offset", default=0, type=int, help="Skip N results")
|
|
78
|
+
@click.option("--min-score", default=0.3, type=float, help="Minimum similarity score")
|
|
79
|
+
@click.option("--refresh", is_flag=True, help="Bypass cache")
|
|
80
|
+
@click.pass_context
|
|
81
|
+
def tools_search(ctx, query, limit, offset, min_score, refresh):
|
|
82
|
+
"""Search tools by natural language query."""
|
|
83
|
+
data = _client(ctx).search_tools(query, limit=limit, offset=offset, min_score=min_score, refresh=refresh)
|
|
84
|
+
if ctx.obj["raw"]:
|
|
85
|
+
output.print_json(data)
|
|
86
|
+
else:
|
|
87
|
+
output.print_search_results(data.get("result", {}))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@tools.command("describe")
|
|
91
|
+
@click.option("--server-id", "-s", required=True, help="Server identifier")
|
|
92
|
+
@click.option("--tool-name", "-t", required=True, multiple=True, help="Tool name (repeatable)")
|
|
93
|
+
@click.option("--refresh", is_flag=True, help="Bypass cache")
|
|
94
|
+
@click.pass_context
|
|
95
|
+
def tools_describe(ctx, server_id, tool_name, refresh):
|
|
96
|
+
"""Get detailed schema for one or more tools."""
|
|
97
|
+
tools_list = [{"server_id": server_id, "tool_name": t} for t in tool_name]
|
|
98
|
+
data = _client(ctx).describe_tools(tools_list, refresh=refresh)
|
|
99
|
+
if ctx.obj["raw"]:
|
|
100
|
+
output.print_json(data)
|
|
101
|
+
else:
|
|
102
|
+
output.print_tool_details(data.get("result", {}).get("results", []))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@tools.command("execute")
|
|
106
|
+
@click.option("--server-id", "-s", required=True, help="Server identifier")
|
|
107
|
+
@click.option("--tool-name", "-t", required=True, help="Tool name")
|
|
108
|
+
@click.option("--args", "-a", "tool_args", default=None, help="Tool arguments as JSON string")
|
|
109
|
+
@click.option("--confirm", "do_confirm", is_flag=True, help="Auto-obtain confirmation token for write ops")
|
|
110
|
+
@click.pass_context
|
|
111
|
+
def tools_execute(ctx, server_id, tool_name, tool_args, do_confirm):
|
|
112
|
+
"""Execute a tool."""
|
|
113
|
+
client = _client(ctx)
|
|
114
|
+
parsed_args = json.loads(tool_args) if tool_args else None
|
|
115
|
+
|
|
116
|
+
confirmation_token = None
|
|
117
|
+
if do_confirm:
|
|
118
|
+
confirm_data = client.confirm_write()
|
|
119
|
+
confirmation_token = confirm_data.get("result", {}).get("confirmation_token")
|
|
120
|
+
|
|
121
|
+
data = client.execute_tool(
|
|
122
|
+
server_id=server_id,
|
|
123
|
+
tool_name=tool_name,
|
|
124
|
+
tool_args=parsed_args,
|
|
125
|
+
confirmed=do_confirm,
|
|
126
|
+
confirmation_token=confirmation_token,
|
|
127
|
+
)
|
|
128
|
+
if ctx.obj["raw"]:
|
|
129
|
+
output.print_json(data)
|
|
130
|
+
else:
|
|
131
|
+
output.print_execute_result(data)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@tools.command("confirm")
|
|
135
|
+
@click.pass_context
|
|
136
|
+
def tools_confirm(ctx):
|
|
137
|
+
"""Get a confirmation token for write operations."""
|
|
138
|
+
data = _client(ctx).confirm_write()
|
|
139
|
+
if ctx.obj["raw"]:
|
|
140
|
+
output.print_json(data)
|
|
141
|
+
else:
|
|
142
|
+
output.print_confirm(data.get("result", {}))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# venn workflow
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
@main.group()
|
|
150
|
+
def workflow():
|
|
151
|
+
"""Execute code in a sandboxed environment."""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@workflow.command("run")
|
|
155
|
+
@click.option("--code", "-c", help="Python code to execute")
|
|
156
|
+
@click.option("--file", "-f", "code_file", type=click.Path(exists=True), help="Read code from file")
|
|
157
|
+
@click.option("--timeout", default=180, type=int, help="Max execution time in seconds")
|
|
158
|
+
@click.option("--confirm", "do_confirm", is_flag=True, help="Obtain confirmation for write ops")
|
|
159
|
+
@click.pass_context
|
|
160
|
+
def workflow_run(ctx, code, code_file, timeout, do_confirm):
|
|
161
|
+
"""Execute Python code in the Venn sandbox."""
|
|
162
|
+
if code_file:
|
|
163
|
+
code = Path(code_file).read_text()
|
|
164
|
+
if not code:
|
|
165
|
+
click.echo("Provide --code or --file", err=True)
|
|
166
|
+
raise SystemExit(1)
|
|
167
|
+
|
|
168
|
+
client = _client(ctx)
|
|
169
|
+
confirmation_token = None
|
|
170
|
+
if do_confirm:
|
|
171
|
+
confirm_data = client.confirm_write()
|
|
172
|
+
confirmation_token = confirm_data.get("result", {}).get("confirmation_token")
|
|
173
|
+
|
|
174
|
+
data = client.execute_workflow(
|
|
175
|
+
code=code,
|
|
176
|
+
timeout=timeout,
|
|
177
|
+
confirmed=do_confirm,
|
|
178
|
+
confirmation_token=confirmation_token,
|
|
179
|
+
)
|
|
180
|
+
if ctx.obj["raw"]:
|
|
181
|
+
output.print_json(data)
|
|
182
|
+
else:
|
|
183
|
+
output.print_execute_result(data)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# venn help
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
@main.command("help")
|
|
191
|
+
@click.argument("action", required=False, type=click.Choice(
|
|
192
|
+
["getting_started", "connector_help", "auth_helper", "list_servers"],
|
|
193
|
+
case_sensitive=False,
|
|
194
|
+
))
|
|
195
|
+
@click.option("--server-id", "-s", help="Server ID (required for auth_helper)")
|
|
196
|
+
@click.option("--refresh", is_flag=True, help="Bypass cache")
|
|
197
|
+
@click.pass_context
|
|
198
|
+
def help_cmd(ctx, action, server_id, refresh):
|
|
199
|
+
"""Get help, server status, or auth URLs."""
|
|
200
|
+
data = _client(ctx).help(action=action, server_id=server_id, refresh=refresh)
|
|
201
|
+
if ctx.obj["raw"]:
|
|
202
|
+
output.print_json(data)
|
|
203
|
+
else:
|
|
204
|
+
output.print_help_result(data.get("result"))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# venn skills
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
@main.group()
|
|
212
|
+
def skills():
|
|
213
|
+
"""Manage executable skills."""
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@skills.command("upsert")
|
|
217
|
+
@click.option("--file", "-f", "skill_file", required=True, type=click.Path(exists=True), help="Skill YAML file")
|
|
218
|
+
@click.pass_context
|
|
219
|
+
def skills_upsert(ctx, skill_file):
|
|
220
|
+
"""Create or update a skill from a YAML file."""
|
|
221
|
+
content = Path(skill_file).read_text()
|
|
222
|
+
data = _client(ctx).upsert_skill(content)
|
|
223
|
+
if ctx.obj["raw"]:
|
|
224
|
+
output.print_json(data)
|
|
225
|
+
else:
|
|
226
|
+
output.console.print("[green]Skill upserted successfully.[/green]")
|
|
227
|
+
if data.get("result"):
|
|
228
|
+
output.print_json(data["result"])
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
# venn history
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
@main.group()
|
|
236
|
+
def history():
|
|
237
|
+
"""View catalog and server revision history."""
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@history.command("toolset")
|
|
241
|
+
@click.argument("toolset_key")
|
|
242
|
+
@click.option("--limit", "-n", default=50, type=int, help="Max revisions")
|
|
243
|
+
@click.option("--before", "before_version", default=None, type=int, help="Only show versions before this")
|
|
244
|
+
@click.pass_context
|
|
245
|
+
def history_toolset(ctx, toolset_key, limit, before_version):
|
|
246
|
+
"""Show revision history for a toolset."""
|
|
247
|
+
data = _client(ctx).toolset_history(toolset_key, limit=limit, before_version=before_version)
|
|
248
|
+
if ctx.obj["raw"]:
|
|
249
|
+
output.print_json(data)
|
|
250
|
+
else:
|
|
251
|
+
output.print_history(data.get("result", {}))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@history.command("server")
|
|
255
|
+
@click.argument("directory_id")
|
|
256
|
+
@click.option("--limit", "-n", default=50, type=int, help="Max revisions")
|
|
257
|
+
@click.option("--before", "before_version", default=None, type=int, help="Only show versions before this")
|
|
258
|
+
@click.pass_context
|
|
259
|
+
def history_server(ctx, directory_id, limit, before_version):
|
|
260
|
+
"""Show revision history for a server instance."""
|
|
261
|
+
data = _client(ctx).server_history(directory_id, limit=limit, before_version=before_version)
|
|
262
|
+
if ctx.obj["raw"]:
|
|
263
|
+
output.print_json(data)
|
|
264
|
+
else:
|
|
265
|
+
output.print_history(data.get("result", {}))
|
venn_cli/client.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
BASE_URL = "https://app.venn.ai/api/tooliq"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VennClient:
|
|
14
|
+
def __init__(self, api_key: str, base_url: str = BASE_URL, timeout: float = 120):
|
|
15
|
+
self._http = httpx.Client(
|
|
16
|
+
base_url=base_url,
|
|
17
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
18
|
+
timeout=timeout,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def _get(self, path: str, params: dict | None = None) -> dict:
|
|
22
|
+
resp = self._http.get(path, params=params)
|
|
23
|
+
return self._handle(resp)
|
|
24
|
+
|
|
25
|
+
def _post(self, path: str, payload: dict | None = None) -> dict:
|
|
26
|
+
resp = self._http.post(path, json=payload or {})
|
|
27
|
+
return self._handle(resp)
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def _handle(resp: httpx.Response) -> dict:
|
|
31
|
+
try:
|
|
32
|
+
data = resp.json()
|
|
33
|
+
except Exception:
|
|
34
|
+
resp.raise_for_status()
|
|
35
|
+
return {}
|
|
36
|
+
if not data.get("success", True):
|
|
37
|
+
err = data.get("error", resp.text)
|
|
38
|
+
print(f"API error: {err}", file=sys.stderr)
|
|
39
|
+
raise SystemExit(1)
|
|
40
|
+
resp.raise_for_status()
|
|
41
|
+
return data
|
|
42
|
+
|
|
43
|
+
# -- tools ---------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def list_tools(
|
|
46
|
+
self,
|
|
47
|
+
slug: str | None = None,
|
|
48
|
+
directory_id: str | None = None,
|
|
49
|
+
instance_id: str | None = None,
|
|
50
|
+
) -> dict:
|
|
51
|
+
params: dict[str, Any] = {}
|
|
52
|
+
if slug:
|
|
53
|
+
params["slug"] = slug
|
|
54
|
+
if directory_id:
|
|
55
|
+
params["directory_id"] = directory_id
|
|
56
|
+
if instance_id:
|
|
57
|
+
params["instance_id"] = instance_id
|
|
58
|
+
return self._get("/tools/list", params)
|
|
59
|
+
|
|
60
|
+
def search_tools(
|
|
61
|
+
self,
|
|
62
|
+
query: str,
|
|
63
|
+
limit: int = 10,
|
|
64
|
+
offset: int = 0,
|
|
65
|
+
min_score: float = 0.3,
|
|
66
|
+
refresh: bool = False,
|
|
67
|
+
) -> dict:
|
|
68
|
+
return self._post("/tools/search", {
|
|
69
|
+
"query": query,
|
|
70
|
+
"limit": limit,
|
|
71
|
+
"offset": offset,
|
|
72
|
+
"min_score": min_score,
|
|
73
|
+
"refresh": refresh,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
def describe_tools(
|
|
77
|
+
self,
|
|
78
|
+
tools: list[dict[str, str]],
|
|
79
|
+
refresh: bool = False,
|
|
80
|
+
) -> dict:
|
|
81
|
+
return self._post("/tools/describe", {"tools": tools, "refresh": refresh})
|
|
82
|
+
|
|
83
|
+
def execute_tool(
|
|
84
|
+
self,
|
|
85
|
+
server_id: str,
|
|
86
|
+
tool_name: str,
|
|
87
|
+
tool_args: dict | None = None,
|
|
88
|
+
confirmed: bool = False,
|
|
89
|
+
confirmation_token: str | None = None,
|
|
90
|
+
) -> dict:
|
|
91
|
+
payload: dict[str, Any] = {
|
|
92
|
+
"server_id": server_id,
|
|
93
|
+
"tool_name": tool_name,
|
|
94
|
+
"confirmed": confirmed,
|
|
95
|
+
}
|
|
96
|
+
if tool_args:
|
|
97
|
+
payload["tool_args"] = tool_args
|
|
98
|
+
if confirmation_token:
|
|
99
|
+
payload["confirmation_token"] = confirmation_token
|
|
100
|
+
return self._post("/tools/execute", payload)
|
|
101
|
+
|
|
102
|
+
def confirm_write(self) -> dict:
|
|
103
|
+
return self._post("/tools/confirm")
|
|
104
|
+
|
|
105
|
+
# -- workflow ------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def execute_workflow(
|
|
108
|
+
self,
|
|
109
|
+
code: str,
|
|
110
|
+
timeout: int = 180,
|
|
111
|
+
confirmed: bool = False,
|
|
112
|
+
confirmation_token: str | None = None,
|
|
113
|
+
) -> dict:
|
|
114
|
+
payload: dict[str, Any] = {
|
|
115
|
+
"code": code,
|
|
116
|
+
"timeout": timeout,
|
|
117
|
+
"confirmed": confirmed,
|
|
118
|
+
}
|
|
119
|
+
if confirmation_token:
|
|
120
|
+
payload["confirmation_token"] = confirmation_token
|
|
121
|
+
return self._post("/tools/execute-workflow", payload)
|
|
122
|
+
|
|
123
|
+
# -- help ----------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def help(
|
|
126
|
+
self,
|
|
127
|
+
action: str | None = None,
|
|
128
|
+
server_id: str | None = None,
|
|
129
|
+
refresh: bool = False,
|
|
130
|
+
) -> dict:
|
|
131
|
+
payload: dict[str, Any] = {"refresh": refresh}
|
|
132
|
+
if action:
|
|
133
|
+
payload["action"] = action
|
|
134
|
+
if server_id:
|
|
135
|
+
payload["server_id"] = server_id
|
|
136
|
+
return self._post("/tools/help", payload)
|
|
137
|
+
|
|
138
|
+
# -- skills --------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def upsert_skill(self, content: str) -> dict:
|
|
141
|
+
return self._post("/skills", {"content": content})
|
|
142
|
+
|
|
143
|
+
# -- history -------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def toolset_history(
|
|
146
|
+
self,
|
|
147
|
+
toolset_key: str,
|
|
148
|
+
limit: int = 50,
|
|
149
|
+
before_version: int | None = None,
|
|
150
|
+
) -> dict:
|
|
151
|
+
params: dict[str, Any] = {"limit": limit}
|
|
152
|
+
if before_version is not None:
|
|
153
|
+
params["before_version"] = before_version
|
|
154
|
+
return self._get(f"/toolset/{toolset_key}/history", params)
|
|
155
|
+
|
|
156
|
+
def server_history(
|
|
157
|
+
self,
|
|
158
|
+
directory_id: str,
|
|
159
|
+
limit: int = 50,
|
|
160
|
+
before_version: int | None = None,
|
|
161
|
+
) -> dict:
|
|
162
|
+
params: dict[str, Any] = {"limit": limit}
|
|
163
|
+
if before_version is not None:
|
|
164
|
+
params["before_version"] = before_version
|
|
165
|
+
return self._get(f"/server/{directory_id}/history", params)
|
venn_cli/output.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.syntax import Syntax
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
err_console = Console(stderr=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def print_json(data: Any) -> None:
|
|
17
|
+
console.print(Syntax(json.dumps(data, indent=2), "json"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def print_tool_list(result: dict) -> None:
|
|
21
|
+
name = result.get("name", "")
|
|
22
|
+
slug = result.get("slug", "")
|
|
23
|
+
version = result.get("toolset_version", "")
|
|
24
|
+
tools = result.get("tools", [])
|
|
25
|
+
|
|
26
|
+
header = f"{name}"
|
|
27
|
+
if slug:
|
|
28
|
+
header += f" ({slug})"
|
|
29
|
+
if version:
|
|
30
|
+
header += f" v{version}"
|
|
31
|
+
|
|
32
|
+
table = Table(title=header, show_lines=False)
|
|
33
|
+
table.add_column("Tool", style="cyan", no_wrap=True)
|
|
34
|
+
table.add_column("Description")
|
|
35
|
+
table.add_column("CRUD", style="dim", width=8)
|
|
36
|
+
table.add_column("Risk", justify="center", width=5)
|
|
37
|
+
table.add_column("Status", style="dim", width=10)
|
|
38
|
+
|
|
39
|
+
for t in tools:
|
|
40
|
+
risk = str(t.get("risk_level", ""))
|
|
41
|
+
risk_style = "green" if int(risk or 0) <= 1 else "yellow" if int(risk or 0) <= 3 else "red"
|
|
42
|
+
table.add_row(
|
|
43
|
+
t["tool_name"],
|
|
44
|
+
t.get("short_description", ""),
|
|
45
|
+
t.get("crud_operation") or "",
|
|
46
|
+
Text(risk, style=risk_style),
|
|
47
|
+
t.get("status", ""),
|
|
48
|
+
)
|
|
49
|
+
console.print(table)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def print_search_results(result: dict) -> None:
|
|
53
|
+
candidates = result.get("candidates", [])
|
|
54
|
+
total = result.get("total", 0)
|
|
55
|
+
|
|
56
|
+
if not candidates:
|
|
57
|
+
console.print("[dim]No results found.[/dim]")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
table = Table(title=f"Search Results ({total} total)", show_lines=False)
|
|
61
|
+
table.add_column("#", style="dim", width=4, justify="right")
|
|
62
|
+
table.add_column("Type", width=6)
|
|
63
|
+
table.add_column("Server / Name", style="cyan")
|
|
64
|
+
table.add_column("Tool / Skill")
|
|
65
|
+
table.add_column("Description")
|
|
66
|
+
|
|
67
|
+
for c in candidates:
|
|
68
|
+
rank = str(c.get("rank", ""))
|
|
69
|
+
if c.get("type") == "skill":
|
|
70
|
+
table.add_row(rank, "skill", "", c["name"], c.get("description", ""))
|
|
71
|
+
else:
|
|
72
|
+
connected = "[green]●[/green]" if c.get("server_connected") else "[red]○[/red]"
|
|
73
|
+
table.add_row(
|
|
74
|
+
rank,
|
|
75
|
+
f"tool {connected}",
|
|
76
|
+
c.get("server_name", c.get("server_id", "")),
|
|
77
|
+
c.get("tool_name", ""),
|
|
78
|
+
c.get("short_description", ""),
|
|
79
|
+
)
|
|
80
|
+
console.print(table)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def print_tool_details(results: list[dict]) -> None:
|
|
84
|
+
for t in results:
|
|
85
|
+
title = f"{t.get('server_name', '')} / {t['tool_name']}"
|
|
86
|
+
body_parts = []
|
|
87
|
+
if t.get("description"):
|
|
88
|
+
body_parts.append(t["description"])
|
|
89
|
+
if t.get("extended_description"):
|
|
90
|
+
body_parts.append(f"\n{t['extended_description']}")
|
|
91
|
+
if t.get("write_operation"):
|
|
92
|
+
body_parts.append(f"\n[yellow]Write operation:[/yellow] {t['write_operation']}")
|
|
93
|
+
|
|
94
|
+
console.print(Panel("\n".join(body_parts), title=title, border_style="cyan"))
|
|
95
|
+
|
|
96
|
+
if t.get("inputSchema"):
|
|
97
|
+
console.print(Syntax(json.dumps(t["inputSchema"], indent=2), "json", theme="monokai"))
|
|
98
|
+
console.print()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def print_execute_result(data: dict) -> None:
|
|
102
|
+
result = data.get("result")
|
|
103
|
+
if isinstance(result, (dict, list)):
|
|
104
|
+
print_json(result)
|
|
105
|
+
elif result is not None:
|
|
106
|
+
console.print(result)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def print_confirm(result: dict) -> None:
|
|
110
|
+
token = result.get("confirmation_token", "")
|
|
111
|
+
expires = result.get("expires_in_seconds", "")
|
|
112
|
+
console.print(f"[green]Confirmation token:[/green] {token}")
|
|
113
|
+
console.print(f"[dim]Expires in {expires}s[/dim]")
|
|
114
|
+
console.print(f"\n[dim]{result.get('usage', '')}[/dim]")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def print_history(result: dict) -> None:
|
|
118
|
+
key = result.get("toolset_key") or result.get("directory_id", "")
|
|
119
|
+
revisions = result.get("revisions", [])
|
|
120
|
+
|
|
121
|
+
if not revisions:
|
|
122
|
+
console.print("[dim]No history found.[/dim]")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
table = Table(title=f"History: {key}", show_lines=False)
|
|
126
|
+
table.add_column("Version", justify="right", width=8)
|
|
127
|
+
table.add_column("Date", width=22)
|
|
128
|
+
table.add_column("Source", style="dim")
|
|
129
|
+
table.add_column("Summary")
|
|
130
|
+
|
|
131
|
+
for r in revisions:
|
|
132
|
+
summary_parts = [f"{k}: {v}" for k, v in r.get("summary", {}).items()]
|
|
133
|
+
table.add_row(
|
|
134
|
+
str(r.get("version", "")),
|
|
135
|
+
r.get("issued_at", ""),
|
|
136
|
+
r.get("source") or "",
|
|
137
|
+
", ".join(summary_parts),
|
|
138
|
+
)
|
|
139
|
+
console.print(table)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def print_help_result(result: Any) -> None:
|
|
143
|
+
if isinstance(result, list):
|
|
144
|
+
for item in result:
|
|
145
|
+
if isinstance(item, dict):
|
|
146
|
+
_print_help_item(item)
|
|
147
|
+
else:
|
|
148
|
+
console.print(item)
|
|
149
|
+
elif isinstance(result, dict):
|
|
150
|
+
_print_help_item(result)
|
|
151
|
+
else:
|
|
152
|
+
console.print(result)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _print_help_item(item: dict) -> None:
|
|
156
|
+
name = item.get("name") or item.get("server_name") or ""
|
|
157
|
+
status = item.get("status") or item.get("connected") or ""
|
|
158
|
+
if name:
|
|
159
|
+
indicator = "[green]●[/green]" if status in (True, "connected") else "[red]○[/red]"
|
|
160
|
+
console.print(f" {indicator} [cyan]{name}[/cyan]", end="")
|
|
161
|
+
remaining = {k: v for k, v in item.items() if k not in ("name", "server_name", "status", "connected")}
|
|
162
|
+
if remaining:
|
|
163
|
+
console.print(f" [dim]{remaining}[/dim]")
|
|
164
|
+
else:
|
|
165
|
+
console.print()
|
|
166
|
+
else:
|
|
167
|
+
print_json(item)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: venn-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for the Venn ToolIQ API
|
|
5
|
+
Project-URL: Homepage, https://github.com/moda-labs/venn-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/moda-labs/venn-cli
|
|
7
|
+
Project-URL: Issues, https://github.com/moda-labs/venn-cli/issues
|
|
8
|
+
Author-email: Moda Labs <zach@modalabs.ai>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: click>=8.1
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0
|
|
24
|
+
Requires-Dist: rich>=13.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
venn_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
venn_cli/cli.py,sha256=V9sZv9G4qflHGB_zJu_mTYlo0J9ITJGznPAuzjGULgk,9596
|
|
3
|
+
venn_cli/client.py,sha256=O-BlREKh9ZxkXXy3_YYyHbi2k3SSrd5NSCRddIBuNgI,4952
|
|
4
|
+
venn_cli/output.py,sha256=EvTlQZLUxsMuNnd_cjvZJsjkPkX6_CscuUeKiLzkUHM,5516
|
|
5
|
+
venn_cli-0.1.0.dist-info/METADATA,sha256=QpcIyub5ngjjzOqWyb2xwjyTatmVeAm0nvK2svMOIT0,1028
|
|
6
|
+
venn_cli-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
venn_cli-0.1.0.dist-info/entry_points.txt,sha256=r47AOFwFdnb-ptJsHkvb8SQrvUUDmc2hx_LogYtVh8U,43
|
|
8
|
+
venn_cli-0.1.0.dist-info/licenses/LICENSE,sha256=R1js932ljyYmujRLNCDR8NaPdroPHUXZYBrXAqQ179Y,1066
|
|
9
|
+
venn_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moda Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|