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.
- code_lm-0.1.0.dist-info/METADATA +181 -0
- code_lm-0.1.0.dist-info/RECORD +22 -0
- code_lm-0.1.0.dist-info/WHEEL +5 -0
- code_lm-0.1.0.dist-info/entry_points.txt +2 -0
- code_lm-0.1.0.dist-info/top_level.txt +1 -0
- gemini_cli/__init__.py +5 -0
- gemini_cli/config.py +78 -0
- gemini_cli/main.py +225 -0
- gemini_cli/models/__init__.py +6 -0
- gemini_cli/models/gemini.py +552 -0
- gemini_cli/models/openrouter.py +559 -0
- gemini_cli/tools/__init__.py +85 -0
- gemini_cli/tools/base.py +86 -0
- gemini_cli/tools/directory_tools.py +120 -0
- gemini_cli/tools/file_tools.py +207 -0
- gemini_cli/tools/quality_tools.py +108 -0
- gemini_cli/tools/summarizer_tool.py +144 -0
- gemini_cli/tools/system_tools.py +65 -0
- gemini_cli/tools/task_complete_tool.py +53 -0
- gemini_cli/tools/test_runner.py +99 -0
- gemini_cli/tools/tree_tool.py +92 -0
- gemini_cli/utils.py +20 -0
|
@@ -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
|