cliver 0.0.1__tar.gz

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 (43) hide show
  1. cliver-0.0.1/PKG-INFO +24 -0
  2. cliver-0.0.1/pyproject.toml +73 -0
  3. cliver-0.0.1/src/cliver/__init__.py +6 -0
  4. cliver-0.0.1/src/cliver/builtin_tools.py +83 -0
  5. cliver-0.0.1/src/cliver/cli.py +170 -0
  6. cliver-0.0.1/src/cliver/cliver_run.py +4 -0
  7. cliver-0.0.1/src/cliver/commands/__init__.py +61 -0
  8. cliver-0.0.1/src/cliver/commands/capabilities.py +71 -0
  9. cliver-0.0.1/src/cliver/commands/chat.py +428 -0
  10. cliver-0.0.1/src/cliver/commands/config.py +60 -0
  11. cliver-0.0.1/src/cliver/commands/llm.py +222 -0
  12. cliver-0.0.1/src/cliver/commands/mcp.py +253 -0
  13. cliver-0.0.1/src/cliver/commands/workflow.py +213 -0
  14. cliver-0.0.1/src/cliver/config.py +551 -0
  15. cliver-0.0.1/src/cliver/constants.py +8 -0
  16. cliver-0.0.1/src/cliver/llm/__init__.py +14 -0
  17. cliver-0.0.1/src/cliver/llm/base.py +174 -0
  18. cliver-0.0.1/src/cliver/llm/llm.py +746 -0
  19. cliver-0.0.1/src/cliver/llm/media_utils.py +155 -0
  20. cliver-0.0.1/src/cliver/llm/ollama_engine.py +244 -0
  21. cliver-0.0.1/src/cliver/llm/openai_engine.py +408 -0
  22. cliver-0.0.1/src/cliver/mcp_server_caller.py +130 -0
  23. cliver-0.0.1/src/cliver/media.py +234 -0
  24. cliver-0.0.1/src/cliver/media_handler.py +245 -0
  25. cliver-0.0.1/src/cliver/model_capabilities.py +175 -0
  26. cliver-0.0.1/src/cliver/prompt_enhancer.py +416 -0
  27. cliver-0.0.1/src/cliver/tools/__init__.py +4 -0
  28. cliver-0.0.1/src/cliver/util.py +343 -0
  29. cliver-0.0.1/src/cliver/workflow/README.md +242 -0
  30. cliver-0.0.1/src/cliver/workflow/__init__.py +21 -0
  31. cliver-0.0.1/src/cliver/workflow/persistence/__init__.py +15 -0
  32. cliver-0.0.1/src/cliver/workflow/persistence/base.py +106 -0
  33. cliver-0.0.1/src/cliver/workflow/persistence/local_cache.py +350 -0
  34. cliver-0.0.1/src/cliver/workflow/steps/__init__.py +18 -0
  35. cliver-0.0.1/src/cliver/workflow/steps/base.py +179 -0
  36. cliver-0.0.1/src/cliver/workflow/steps/function_step.py +95 -0
  37. cliver-0.0.1/src/cliver/workflow/steps/human_step.py +88 -0
  38. cliver-0.0.1/src/cliver/workflow/steps/llm_step.py +131 -0
  39. cliver-0.0.1/src/cliver/workflow/steps/workflow_step.py +88 -0
  40. cliver-0.0.1/src/cliver/workflow/workflow_executor.py +409 -0
  41. cliver-0.0.1/src/cliver/workflow/workflow_manager_base.py +32 -0
  42. cliver-0.0.1/src/cliver/workflow/workflow_manager_local.py +144 -0
  43. cliver-0.0.1/src/cliver/workflow/workflow_models.py +130 -0
cliver-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.3
2
+ Name: cliver
3
+ Version: 0.0.1
4
+ Summary: An AI Agent to make your CLI clever and more
5
+ Author: Lin Gao
6
+ Author-email: Lin Gao <aoingl@gmail.com>
7
+ Requires-Dist: click>=8.1.0
8
+ Requires-Dist: rich>=13.0.0
9
+ Requires-Dist: httpx>=0.24.0
10
+ Requires-Dist: pydantic>=2.0.0
11
+ Requires-Dist: prompt-toolkit>=3.0.0
12
+ Requires-Dist: pyyaml>=6.0.0
13
+ Requires-Dist: mcp>=1.2.1
14
+ Requires-Dist: openai>=1.0.0
15
+ Requires-Dist: langchain>=0.3.0
16
+ Requires-Dist: langchain-mcp-adapters>=0.1.4
17
+ Requires-Dist: langchain-openai>=0.3.18
18
+ Requires-Dist: langchain-core>=0.3.63
19
+ Requires-Dist: langchain-ollama>=0.3.3
20
+ Requires-Dist: python-dotenv>=1.1.0
21
+ Requires-Dist: jinja2>=3.1.0
22
+ Requires-Dist: requests>=2.32.3
23
+ Requires-Dist: json-repair>=0.52.0
24
+ Requires-Python: >=3.10
@@ -0,0 +1,73 @@
1
+ [project]
2
+ name = "cliver"
3
+ version = "0.0.1"
4
+ description = "An AI Agent to make your CLI clever and more"
5
+ authors = [
6
+ { name = "Lin Gao", email = "aoingl@gmail.com" }
7
+ ]
8
+ requires-python = ">=3.10"
9
+
10
+ dependencies = [
11
+ "click>=8.1.0",
12
+ "rich>=13.0.0",
13
+ "httpx>=0.24.0",
14
+ "pydantic>=2.0.0",
15
+ "prompt-toolkit>=3.0.0",
16
+ "pyyaml>=6.0.0",
17
+ "mcp>=1.2.1",
18
+ "openai>=1.0.0",
19
+ "langchain>=0.3.0",
20
+ "langchain-mcp-adapters>=0.1.4",
21
+ "langchain-openai>=0.3.18",
22
+ "langchain-core>=0.3.63",
23
+ "langchain-ollama>=0.3.3",
24
+ "python-dotenv>=1.1.0",
25
+ "jinja2>=3.1.0",
26
+ "requests>=2.32.3",
27
+ "json-repair>=0.52.0",
28
+ ]
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "pytest>=7.0.0",
33
+ "pytest-cov",
34
+ "pytest-asyncio>=1.2.0",
35
+ "isort>=5.12.0",
36
+ "mypy>=1.0.0",
37
+ "ruff>=0.0.270",
38
+ "black>=25.1.0",
39
+ ]
40
+
41
+ docs = [
42
+ "mkdocs>=1.4.0",
43
+ "mkdocs-material>=9.0.0",
44
+ "mkdocs-minify-plugin>=0.2.0",
45
+ ]
46
+
47
+ [project.scripts]
48
+ cliver = "cliver.cli:cliver_main"
49
+
50
+ [build-system]
51
+ requires = ["uv_build>=0.7,<0.8"]
52
+ build-backend = "uv_build"
53
+
54
+ [tool.uv]
55
+ package = true
56
+
57
+ [tool.mypy]
58
+ python_version = "3.10"
59
+ warn_return_any = true
60
+ warn_unused_configs = true
61
+ disallow_untyped_defs = true
62
+ disallow_incomplete_defs = true
63
+
64
+ [tool.ruff]
65
+ line-length = 88
66
+ target-version = "py310"
67
+ select = ["E", "F", "B", "I"]
68
+
69
+ [tool.pytest.ini_options]
70
+ testpaths = ["tests"]
71
+ asyncio_mode = "auto"
72
+ asyncio_default_fixture_loop_scope = "function"
73
+
@@ -0,0 +1,6 @@
1
+ from dotenv import load_dotenv
2
+
3
+ load_dotenv()
4
+
5
+ from cliver.llm import TaskExecutor
6
+ from cliver.media_handler import MultimediaResponseHandler, MultimediaResponse
@@ -0,0 +1,83 @@
1
+ """Built-in tools that are always available for the LLM to use."""
2
+ import inspect
3
+ from typing import List, Dict, Any, Union
4
+ from langchain_core.tools import BaseTool
5
+ import cliver.tools
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ # The builtin tools should be defined in the module: 'cliver.tools'
11
+ # All builtin tools should have a global unique name either like: 'tool_name' or 'builtin#tool_name'
12
+ def get_builtin_tools() -> List[BaseTool]:
13
+ """
14
+ Return a list of builtin tools that should always be available.
15
+
16
+ Returns:
17
+ List of BaseTool objects representing builtin tools
18
+ """
19
+
20
+ tools: List[BaseTool] = []
21
+ for name, obj in inspect.getmembers(cliver.tools):
22
+ if isinstance(obj, BaseTool):
23
+ logger.debug(f"Register builtin tool: {name}")
24
+ tools.append(obj)
25
+ return tools
26
+
27
+
28
+ class BuiltinTools:
29
+ """
30
+ A class to manage built-in tools with caching and execution capabilities.
31
+ """
32
+
33
+ def __init__(self):
34
+ self._tools = None # Cache for builtin tools
35
+
36
+ @property
37
+ def tools(self) -> List[BaseTool]:
38
+ """Get the list of builtin tools, caching them on first access."""
39
+ if self._tools is None:
40
+ self._tools = get_builtin_tools()
41
+ return self._tools
42
+
43
+ def execute_tool(self, tool_name: str, args: Union[str, Dict[str, Any]] = None) -> List[Dict[str, Any]]:
44
+ """
45
+ Execute a builtin tool by name with the provided arguments.
46
+
47
+ Args:
48
+ tool_name (str): The name of the tool to execute
49
+ args (Dict[str, Any]): Arguments to pass to the tool
50
+
51
+ Returns:
52
+ List[Dict[str, Any]]: The result of the tool execution
53
+ """
54
+ if args is None:
55
+ args = {}
56
+
57
+ # Find the tool by name
58
+ tool = None
59
+ for t in self.tools:
60
+ if t.name == tool_name or t.name == f"builtin#{tool_name}":
61
+ tool = t
62
+ break
63
+
64
+ if tool is None:
65
+ return [{"error": f"Tool '{tool_name}' not found in builtin tools"}]
66
+
67
+ try:
68
+ # Execute the tool
69
+ result = tool.invoke(input=args)
70
+
71
+ # Handle different result types
72
+ if isinstance(result, dict):
73
+ return [result]
74
+ elif isinstance(result, list):
75
+ return result
76
+ elif result is None:
77
+ return [{}]
78
+ else:
79
+ return [{"tool_result": str(result)}]
80
+
81
+ except Exception as e:
82
+ logger.error(f"Failed to execute builtin tool {tool_name}", exc_info=e)
83
+ return [{"error": str(e)}]
@@ -0,0 +1,170 @@
1
+ """
2
+ Cliver CLI Module
3
+
4
+ The main entrance of the cliver application
5
+ """
6
+
7
+ from shlex import split as shell_split
8
+ import click
9
+ import sys
10
+ from prompt_toolkit import PromptSession
11
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
12
+ from prompt_toolkit.completion import WordCompleter
13
+ from prompt_toolkit.history import FileHistory
14
+ from prompt_toolkit.styles import Style
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+
18
+ from cliver.config import ConfigManager
19
+ from cliver.llm import TaskExecutor
20
+ from cliver import commands
21
+ from cliver.util import get_config_dir, stdin_is_piped, read_piped_input
22
+ from cliver.constants import *
23
+
24
+
25
+ class Cliver:
26
+ """
27
+ The global App is the box for all capabilities.
28
+
29
+ """
30
+
31
+ def __init__(self):
32
+ """Initialize the Cliver application."""
33
+ # load config
34
+ self.config_dir = get_config_dir()
35
+ dir_str = str(self.config_dir.absolute())
36
+ if dir_str not in sys.path:
37
+ sys.path.append(dir_str)
38
+ self.config_manager = ConfigManager(self.config_dir)
39
+ self.task_executor = TaskExecutor(
40
+ llm_models=self.config_manager.list_llm_models(),
41
+ mcp_servers=self.config_manager.list_mcp_servers_for_mcp_caller(),
42
+ default_model=self.config_manager.get_llm_model(),
43
+ )
44
+ # prepare console for interaction
45
+ self.history_path = self.config_dir / "history"
46
+ self.session = None
47
+ self.console = Console()
48
+ self.piped = stdin_is_piped()
49
+
50
+ # TODO: we may need to maintain the selected model, and other options during the session initiation
51
+ def init_session(self, group: click.Group):
52
+ if self.piped or self.session is not None:
53
+ return
54
+ self.session = PromptSession(
55
+ history=FileHistory(str(self.history_path)),
56
+ auto_suggest=AutoSuggestFromHistory(),
57
+ completer=WordCompleter(self.load_commands_names(group), ignore_case=True),
58
+ style=Style.from_dict(
59
+ {
60
+ "prompt": "ansigreen bold",
61
+ }
62
+ ),
63
+ )
64
+
65
+ def load_commands_names(self, group: click.Group) -> list[str]:
66
+ return commands.list_commands_names(group)
67
+
68
+ def run(self) -> None:
69
+ """Run the Cliver client."""
70
+ if self.piped:
71
+ user_data = read_piped_input()
72
+ if user_data is None:
73
+ self.console.print(
74
+ "[bold yellow]No data received from stdin.[/bold yellow]"
75
+ )
76
+ else:
77
+ if not user_data.lower() in ("exit", "quit"):
78
+ self.call_cmd(user_data)
79
+ else:
80
+ self.console.print(
81
+ Panel.fit(
82
+ "[bold blue]CLIver[/bold blue] - AI Agent Command Line Interface",
83
+ border_style="blue",
84
+ )
85
+ )
86
+ self.console.print(
87
+ "Type [bold green]/help[/bold green] to see available commands or start typing to interact with the AI."
88
+ )
89
+
90
+ while True:
91
+ try:
92
+ # Get user input
93
+ line = self.session.prompt("Cliver> ").strip()
94
+ if line.lower() in ("exit", "quit", "/exit", "/quit"):
95
+ break
96
+ if line.startswith("/") and len(line) > 1:
97
+ # possibly a command
98
+ line = line[1:]
99
+ elif not line.lower().startswith(f"{CMD_CHAT} "):
100
+ line = f"{CMD_CHAT} {line}"
101
+
102
+ self.call_cmd(line)
103
+
104
+ except KeyboardInterrupt:
105
+ self.console.print(
106
+ "\n[yellow]Use 'exit' or 'quit' to exit[/yellow]"
107
+ )
108
+ except EOFError:
109
+ break
110
+ except click.exceptions.NoArgsIsHelpError as e:
111
+ self.console.print(f"{e.format_message()}")
112
+ except Exception as e:
113
+ self.console.print(f"[red]Error: {e}[/red]")
114
+
115
+ # Clean up
116
+ self.cleanup()
117
+
118
+ def call_cmd(self, line: str):
119
+ """
120
+ Call a command with the given name and arguments.
121
+ """
122
+ parts = shell_split(line)
123
+ cliver(args=parts, prog_name="cliver", standalone_mode=False, obj=self)
124
+
125
+ def cleanup(self):
126
+ """
127
+ Clean up the resources opened by the application like the mcp server processes or connections to remote mcp servers, llm providers
128
+ """
129
+ # TODO
130
+ pass
131
+
132
+
133
+ pass_cliver = click.make_pass_decorator(Cliver)
134
+
135
+
136
+ @click.group(invoke_without_command=True)
137
+ @click.pass_context
138
+ def cliver(ctx: click.Context):
139
+ """
140
+ Cliver: An application aims to make your CLI clever
141
+ """
142
+ cli = None
143
+ if ctx.obj is None:
144
+ cli = Cliver()
145
+ ctx.obj = cli
146
+
147
+ if ctx.invoked_subcommand is None:
148
+ # If no subcommand is invoked, show the help message
149
+ interact(cli)
150
+
151
+
152
+ def interact(cli: Cliver):
153
+ """
154
+ Start an interactive session with the AI agent.
155
+ """
156
+ cli.init_session(cliver)
157
+ cli.run()
158
+
159
+
160
+ def loads_commands():
161
+ commands.loads_commands(cliver)
162
+ # Loads extended commands from config dir
163
+ commands.loads_external_commands(cliver)
164
+
165
+
166
+ def cliver_main(*args, **kwargs):
167
+ # loading all click groups and commands before calling it
168
+ loads_commands()
169
+ # bootstrap the cliver application
170
+ cliver()
@@ -0,0 +1,4 @@
1
+ from cliver.cli import cliver_main
2
+
3
+ if __name__ == "__main__":
4
+ cliver_main()
@@ -0,0 +1,61 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ import click
5
+ import importlib
6
+ from typing import List, Callable
7
+ from cliver.util import get_config_dir
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ def loads_commands(group: click.Group) -> None:
12
+ current_dir = os.path.dirname(os.path.abspath(__file__))
13
+ _load_commands_from_dir(
14
+ current_dir,
15
+ group,
16
+ package_name="cliver.commands",
17
+ filter_fn=lambda f_name: f_name != "__init__.py",
18
+ )
19
+
20
+ # This will load py modules from config directory
21
+ # This assumes the py modules are safe and should be set up manually.
22
+ def loads_external_commands(group: click.Group) -> None:
23
+ config_dir = get_config_dir()
24
+ dir_str = str(config_dir.absolute() / "commands")
25
+ if dir_str not in sys.path:
26
+ sys.path.append(dir_str)
27
+ _load_commands_from_dir(dir_str, group, log=True)
28
+
29
+ def _load_commands_from_dir(
30
+ commands_dir: str,
31
+ group: click.Group,
32
+ package_name: str = None,
33
+ filter_fn: Callable[[str], bool] = None,
34
+ log: bool = False,
35
+ ) -> None:
36
+ if commands_dir and not os.path.exists(commands_dir):
37
+ logger.warning("Commands directory: %s does not exist", commands_dir)
38
+ return
39
+ for filename in os.listdir(commands_dir):
40
+ if filename.endswith(".py"):
41
+ # either we don't filter or filter_fn returns True
42
+ if filter_fn is None or filter_fn(filename):
43
+ if log:
44
+ full = os.path.abspath(os.path.join(commands_dir, filename))
45
+ logger.debug("Loads command from: %s", full)
46
+ grp_name = filename[:-3]
47
+ module_name = f"{grp_name}"
48
+ if package_name is not None:
49
+ module_name = f"{package_name}.{grp_name}"
50
+ module = importlib.import_module(module_name)
51
+ if hasattr(module, grp_name):
52
+ cli_obj = getattr(module, grp_name)
53
+ if isinstance(cli_obj, click.Command):
54
+ group.add_command(cli_obj)
55
+ if hasattr(module, "post_group"):
56
+ pg_obj = getattr(module, "post_group")
57
+ pg_obj()
58
+
59
+
60
+ def list_commands_names(group: click.Group) -> List[str]:
61
+ return [name for name, _ in group.commands.items()]
@@ -0,0 +1,71 @@
1
+ """
2
+ Model capabilities command for Cliver client.
3
+ """
4
+
5
+ import click
6
+ from rich.table import Table
7
+
8
+ from cliver.cli import Cliver, pass_cliver
9
+
10
+
11
+ @click.command()
12
+ @click.option("--model", "-m", help="Model name to check capabilities for")
13
+ @click.option(
14
+ "--detailed", "-d", is_flag=True, help="Show detailed modality capabilities"
15
+ )
16
+ @pass_cliver
17
+ def capabilities(cli: Cliver, model: str = None, detailed: bool = False):
18
+ """Display model capabilities."""
19
+ models = cli.config_manager.list_llm_models()
20
+
21
+ if not models:
22
+ cli.console.print("[yellow]No models configured.[/yellow]")
23
+ return
24
+
25
+ if model:
26
+ if model not in models:
27
+ cli.console.print(f"[red]Model '{model}' not found.[/red]")
28
+ return
29
+ models = {model: models[model]}
30
+
31
+ if detailed:
32
+ table = Table(title="Detailed Model Capabilities")
33
+ table.add_column("Model", style="cyan")
34
+ table.add_column("Provider", style="magenta")
35
+ table.add_column("Text", style="green")
36
+ table.add_column("Image In", style="blue")
37
+ table.add_column("Image Out", style="blue")
38
+ table.add_column("Audio In", style="yellow")
39
+ table.add_column("Audio Out", style="yellow")
40
+ table.add_column("Video In", style="purple")
41
+ table.add_column("Video Out", style="purple")
42
+ table.add_column("Tools", style="red")
43
+
44
+ for model_name, model_config in models.items():
45
+ capabilities = model_config.get_model_capabilities()
46
+ modality_caps = capabilities.get_modality_capabilities()
47
+ table.add_row(
48
+ model_name,
49
+ model_config.provider,
50
+ "✓" if modality_caps["text"] else "✗",
51
+ "✓" if modality_caps["image_input"] else "✗",
52
+ "✓" if modality_caps["image_output"] else "✗",
53
+ "✓" if modality_caps["audio_input"] else "✗",
54
+ "✓" if modality_caps["audio_output"] else "✗",
55
+ "✓" if modality_caps["video_input"] else "✗",
56
+ "✓" if modality_caps["video_output"] else "✗",
57
+ "✓" if modality_caps["tool_calling"] else "✗",
58
+ )
59
+ else:
60
+ table = Table(title="Model Capabilities")
61
+ table.add_column("Model", style="cyan")
62
+ table.add_column("Provider", style="magenta")
63
+ table.add_column("Capabilities", style="green")
64
+
65
+ for model_name, model_config in models.items():
66
+ capabilities = model_config.get_capabilities()
67
+ cap_names = [cap.value for cap in capabilities]
68
+ cap_str = ", ".join(cap_names) if cap_names else "None"
69
+ table.add_row(model_name, model_config.provider, cap_str)
70
+
71
+ cli.console.print(table)