code-lm 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.
@@ -0,0 +1,65 @@
1
+ """
2
+ System operation tools.
3
+ """
4
+
5
+ import os
6
+ import subprocess
7
+ import tempfile
8
+ from .base import BaseTool
9
+
10
+ class BashTool(BaseTool):
11
+ """Tool to execute bash commands."""
12
+
13
+ name = "bash"
14
+ description = "Execute a bash command"
15
+
16
+ # List of banned commands for security
17
+ BANNED_COMMANDS = [
18
+ 'curl', 'wget', 'nc', 'netcat', 'telnet',
19
+ 'lynx', 'w3m', 'links', 'ssh',
20
+ ]
21
+
22
+ def execute(self, command, timeout=30000):
23
+ """
24
+ Execute a bash command.
25
+
26
+ Args:
27
+ command: The command to execute
28
+ timeout: Timeout in milliseconds (optional)
29
+ """
30
+ try:
31
+ # Check for banned commands
32
+ for banned in self.BANNED_COMMANDS:
33
+ if banned in command.split():
34
+ return f"Error: The command '{banned}' is not allowed for security reasons."
35
+
36
+ # Convert timeout to seconds (with better error handling)
37
+ try:
38
+ timeout_sec = int(timeout) / 1000
39
+ except ValueError:
40
+ # If timeout can't be converted to int, use default
41
+ timeout_sec = 30
42
+
43
+ # Remove the temporary directory context
44
+ process = subprocess.Popen(
45
+ command,
46
+ shell=True,
47
+ stdout=subprocess.PIPE,
48
+ stderr=subprocess.PIPE,
49
+ text=True,
50
+ )
51
+
52
+ try:
53
+ stdout, stderr = process.communicate(timeout=timeout_sec)
54
+
55
+ if process.returncode != 0:
56
+ return f"Command exited with status {process.returncode}\n\nSTDOUT:\n{stdout}\n\nSTDERR:\n{stderr}"
57
+
58
+ return stdout
59
+
60
+ except subprocess.TimeoutExpired:
61
+ process.kill()
62
+ return f"Error: Command timed out after {timeout_sec} seconds"
63
+
64
+ except Exception as e:
65
+ return f"Error executing command: {str(e)}"
@@ -0,0 +1,53 @@
1
+ """
2
+ Tool to signal task completion.
3
+ """
4
+ import logging
5
+ from .base import BaseTool
6
+
7
+ log = logging.getLogger(__name__)
8
+
9
+ class TaskCompleteTool(BaseTool):
10
+ """
11
+ Signals that the current task/request is fully completed.
12
+ This MUST be the final tool called by the assistant for a given request.
13
+ """
14
+ name = "task_complete"
15
+ description = "Signals task completion. MUST be called as the final step, providing a user-friendly summary."
16
+
17
+ def execute(self, summary: str) -> str:
18
+ """
19
+ Signals completion and returns the summary provided by the LLM.
20
+
21
+ Args:
22
+ summary: A concise, user-friendly summary of the actions taken and the final outcome.
23
+
24
+ Returns:
25
+ The summary string provided as input. The orchestrator uses this call as a signal to stop.
26
+ """
27
+ log.info(f"Task completion signaled by LLM.")
28
+
29
+ # --- ADDED/MODIFIED: More Robust Cleaning ---
30
+ cleaned_summary = summary
31
+ if isinstance(summary, str):
32
+ log.debug(f"Original summary from LLM (Type: {type(summary)}, Length: {len(summary)}): \"{summary}\"")
33
+ # Repeatedly strip common leading/trailing chars like quotes, spaces
34
+ strippable_chars = ' "\'\n\t'
35
+ while cleaned_summary.startswith(tuple(strippable_chars)) or cleaned_summary.endswith(tuple(strippable_chars)):
36
+ prev_summary = cleaned_summary
37
+ cleaned_summary = cleaned_summary.strip(strippable_chars)
38
+ if cleaned_summary == prev_summary: # Avoid infinite loop if strip doesn't change anything
39
+ break
40
+ log.debug(f"Final cleaned summary: \"{cleaned_summary}\"")
41
+ else:
42
+ log.warning(f"TaskCompleteTool received non-string summary type: {type(summary)}")
43
+ cleaned_summary = str(summary).strip() # Attempt to convert and strip
44
+ # --- END ADDED/MODIFIED SECTION ---
45
+
46
+ log.debug(f"Processing summary (cleaned): {cleaned_summary}")
47
+ if not cleaned_summary or len(cleaned_summary) < 5:
48
+ log.warning("TaskCompleteTool called with missing or very short summary.")
49
+ # Provide a default confirmation if summary is bad/missing
50
+ return "Task marked as complete, but the provided summary was insufficient."
51
+ # The orchestrator loop will see this tool was called and use this summary.
52
+ # We just return the summary itself.
53
+ return cleaned_summary
@@ -0,0 +1,99 @@
1
+ """
2
+ Tool for running automated tests (e.g., pytest).
3
+ """
4
+
5
+ import subprocess
6
+ import logging
7
+ import shlex
8
+ from .base import BaseTool
9
+
10
+ # Configure logging for this tool
11
+ log = logging.getLogger(__name__)
12
+
13
+ class TestRunnerTool(BaseTool):
14
+ """
15
+ Tool to execute automated tests using a test runner like pytest.
16
+ Assumes the test runner command (e.g., 'pytest') is available
17
+ in the environment where the CLI is run.
18
+ """
19
+ name = "test_runner"
20
+ description = "Runs automated tests using the project's test runner (defaults to trying 'pytest'). Use after making code changes to verify correctness."
21
+
22
+ def execute(self, test_path: str | None = None, options: str | None = None, runner_command: str = "pytest") -> str:
23
+ """
24
+ Executes automated tests.
25
+
26
+ Args:
27
+ test_path: Specific file or directory to test (optional, runs tests discovered by the runner if omitted).
28
+ options: Additional command-line options for the test runner (e.g., '-k my_test', '-v', '--cov'). Optional.
29
+ runner_command: The command to invoke the test runner (default: 'pytest').
30
+
31
+ Returns:
32
+ A string summarizing the test results, including output on failure.
33
+ """
34
+ command = [runner_command]
35
+
36
+ if options:
37
+ # Use shlex to safely split options string respecting quotes
38
+ try:
39
+ command.extend(shlex.split(options))
40
+ except ValueError as e:
41
+ log.warning(f"Could not parse options string '{options}': {e}. Ignoring options.")
42
+ # Optionally return an error message here
43
+ # return f"Error: Invalid options string provided: {options}"
44
+
45
+ if test_path:
46
+ command.append(test_path)
47
+
48
+ log.info(f"Executing test command: {' '.join(command)}")
49
+
50
+ try:
51
+ # Execute the command, capture output, set a timeout (e.g., 5 minutes)
52
+ process = subprocess.run(
53
+ command,
54
+ capture_output=True,
55
+ text=True,
56
+ check=False, # Don't raise exception on non-zero exit code, we'll check it manually
57
+ timeout=300 # Timeout in seconds (e.g., 5 minutes)
58
+ )
59
+
60
+ exit_code = process.returncode
61
+ stdout = process.stdout.strip()
62
+ stderr = process.stderr.strip()
63
+
64
+ log.info(f"Test run completed. Exit Code: {exit_code}")
65
+ log.debug(f"Test stdout:\n{stdout}")
66
+ if stderr:
67
+ log.debug(f"Test stderr:\n{stderr}")
68
+
69
+ # Prepare summary message
70
+ summary = f"Test run using '{runner_command}' completed.\n"
71
+ summary += f"Exit Code: {exit_code}\n"
72
+
73
+ if exit_code == 0:
74
+ summary += "Status: SUCCESS\n"
75
+ # Include stdout even on success, maybe truncated?
76
+ summary += f"\nOutput:\n---\n{stdout[-1000:]}\n---\n" # Show last 1000 chars
77
+ else:
78
+ summary += "Status: FAILED\n"
79
+ summary += f"\nStandard Output:\n---\n{stdout}\n---\n"
80
+ if stderr:
81
+ summary += f"\nStandard Error:\n---\n{stderr}\n---\n"
82
+
83
+ # Specific exit codes for pytest might be useful
84
+ # (e.g., 5 means no tests collected) - can add more logic here
85
+ if exit_code == 5 and 'pytest' in runner_command:
86
+ summary += "\nNote: Pytest exit code 5 often means no tests were found or collected."
87
+
88
+
89
+ return summary
90
+
91
+ except FileNotFoundError:
92
+ log.error(f"Test runner command '{runner_command}' not found.")
93
+ return f"Error: Test runner command '{runner_command}' not found. Is it installed and in PATH?"
94
+ except subprocess.TimeoutExpired:
95
+ log.error("Test run timed out.")
96
+ return "Error: Test run exceeded the timeout limit (5 minutes)."
97
+ except Exception as e:
98
+ log.error(f"An unexpected error occurred while running tests: {e}", exc_info=True)
99
+ return f"Error: An unexpected error occurred: {str(e)}"
@@ -0,0 +1,92 @@
1
+ """
2
+ Tool for displaying directory structure using the 'tree' command.
3
+ """
4
+ import subprocess
5
+ import logging
6
+ from google.generativeai.types import FunctionDeclaration, Tool
7
+
8
+ from .base import BaseTool
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+ DEFAULT_TREE_DEPTH = 3
13
+ MAX_TREE_DEPTH = 10
14
+
15
+ class TreeTool(BaseTool):
16
+ name: str = "tree"
17
+ description: str = (
18
+ f"""Displays the directory structure as a tree. Shows directories and files.
19
+ Use this to understand the hierarchy and layout of the current working directory or a subdirectory.
20
+ Defaults to a depth of {DEFAULT_TREE_DEPTH}. Use the 'depth' argument to specify a different level.
21
+ Optionally specify a 'path' to view a subdirectory instead of the current directory."""
22
+ )
23
+ args_schema: dict = {
24
+ "path": {
25
+ "type": "string",
26
+ "description": "Optional path to a specific directory relative to the workspace root. If omitted, uses the current directory.",
27
+ },
28
+ "depth": {
29
+ "type": "integer",
30
+ "description": f"Optional maximum display depth of the directory tree (Default: {DEFAULT_TREE_DEPTH}, Max: {MAX_TREE_DEPTH}).",
31
+ },
32
+ }
33
+ # Optional args: path, depth
34
+ required_args: list[str] = []
35
+
36
+ def execute(self, path: str | None = None, depth: int | None = None) -> str:
37
+ """Executes the tree command."""
38
+
39
+ if depth is None:
40
+ depth_limit = DEFAULT_TREE_DEPTH
41
+ else:
42
+ # Clamp depth to be within reasonable limits
43
+ depth_limit = max(1, min(depth, MAX_TREE_DEPTH))
44
+
45
+ command = ['tree', f'-L {depth_limit}']
46
+
47
+ # Add path if specified
48
+ target_path = "." # Default to current directory
49
+ if path:
50
+ # Basic path validation/sanitization might be needed depending on security context
51
+ target_path = path
52
+ command.append(target_path)
53
+
54
+ log.info(f"Executing tree command: {' '.join(command)}")
55
+ try:
56
+ # Adding '-a' might be useful to show hidden files, but could be verbose.
57
+ # Adding '-F' appends / to dirs, * to executables, etc.
58
+ # Using shell=True is generally discouraged, but might be needed if tree isn't directly in PATH
59
+ # or if handling complex paths. Sticking to list format for now.
60
+ process = subprocess.run(
61
+ command,
62
+ capture_output=True,
63
+ text=True,
64
+ check=False, # Don't raise exception on non-zero exit code
65
+ timeout=15 # Add a timeout
66
+ )
67
+
68
+ if process.returncode == 0:
69
+ log.info(f"Tree command successful for path '{target_path}' with depth {depth_limit}.")
70
+ # Limit output size? Tree can be huge.
71
+ output = process.stdout.strip()
72
+ if len(output.splitlines()) > 200: # Limit lines as a proxy for size
73
+ log.warning(f"Tree output for '{target_path}' exceeded 200 lines. Truncating.")
74
+ output = "\n".join(output.splitlines()[:200]) + "\n... (output truncated)"
75
+ return output
76
+ elif process.returncode == 127 or "command not found" in process.stderr.lower():
77
+ log.error(f"\'tree\' command not found. It might not be installed.")
78
+ return "Error: 'tree' command not found. Please ensure it is installed and in the system's PATH."
79
+ else:
80
+ log.error(f"Tree command failed with return code {process.returncode}. Path: '{target_path}', Depth: {depth_limit}. Stderr: {process.stderr.strip()}")
81
+ error_detail = process.stderr.strip() if process.stderr else "(No stderr)"
82
+ return f"Error executing tree command (Code: {process.returncode}): {error_detail}"
83
+
84
+ except FileNotFoundError:
85
+ log.error(f"\'tree\' command not found (FileNotFoundError). It might not be installed.")
86
+ return "Error: 'tree' command not found. Please ensure it is installed and in the system's PATH."
87
+ except subprocess.TimeoutExpired:
88
+ log.error(f"Tree command timed out for path '{target_path}' after 15 seconds.")
89
+ return f"Error: Tree command timed out for path '{target_path}'. The directory might be too large or complex."
90
+ except Exception as e:
91
+ log.exception(f"An unexpected error occurred while executing tree command for path '{target_path}': {e}")
92
+ return f"An unexpected error occurred while executing tree: {str(e)}"
gemini_cli/utils.py ADDED
@@ -0,0 +1,20 @@
1
+ """
2
+ Utility functions for the Gemini CLI tool.
3
+ """
4
+
5
+ import tiktoken
6
+ import json
7
+
8
+ def count_tokens(text):
9
+ """
10
+ Count the number of tokens in a text string.
11
+
12
+ This is a rough estimate for Gemini 2.5 Pro, using GPT-4 tokenizer as a proxy.
13
+ For production, you'd want to use model-specific token counting.
14
+ """
15
+ try:
16
+ encoding = tiktoken.encoding_for_model("gpt-4")
17
+ return len(encoding.encode(text))
18
+ except Exception:
19
+ # Fallback method: roughly 4 chars per token
20
+ return len(text) // 4