diff --git a/hendrik.py b/hendrik.py index 869259f..7290a89 100644 --- a/hendrik.py +++ b/hendrik.py @@ -29,6 +29,8 @@ tools_definition = [ gadget.tools_mapping( schema = coder.schema_git_operation, handler = coder.git_operation ), + gadget.tools_mapping( schema = coder.schema_workspace_change, handler = coder.workspace_change ), + # Carrack Tools gadget.tools_mapping( schema = carrack.schema_sendhttprequest, handler = carrack.sendhttprequest ), @@ -87,7 +89,7 @@ def main(): if not os.path.isdir(resolved): print(f"Error: '{resolved}' is not a valid directory", flush=True) sys.exit(1) - os.chdir(resolved) + coder.set_current_workspace(resolved) llm_client = LLMClient(config.llm_baseurl, config.llm_model, config.llm_api_key, config.llm_timeout) diff --git a/interfaces/tui/input.py b/interfaces/tui/input.py index 0b0789c..b864466 100644 --- a/interfaces/tui/input.py +++ b/interfaces/tui/input.py @@ -6,6 +6,7 @@ import curses import os import config from .agent import submit, log +from tools.coder import set_current_workspace def _build_visual(buffer, max_chars): @@ -173,7 +174,7 @@ def workspace_popup(app, stdscr): if ws: resolved = os.path.abspath(ws) if os.path.isdir(resolved): - os.chdir(resolved) + set_current_workspace(resolved) log(app, "system", f"Workspace \u2192 {resolved}") else: log(app, "error", f"Invalid directory: {resolved}") diff --git a/interfaces/tui/render.py b/interfaces/tui/render.py index 612a1b6..e7dc92f 100644 --- a/interfaces/tui/render.py +++ b/interfaces/tui/render.py @@ -4,7 +4,7 @@ import curses import json -import os +from tools.coder import get_current_workspace # -- Color pair IDs (id 1-9, id 0 = default curses) -- C_HEADER = 1 # header bar: biru @@ -377,7 +377,7 @@ def draw_status(app, stdscr): # Status bar di baris h-9: mode, workspace, session h, w = app.h, app.w y = h - 9 - ws = os.getcwd() + ws = get_current_workspace() session_tag = "" if app.current_session: diff --git a/tools/coder.py b/tools/coder.py index 9f633ae..5c1cd40 100644 --- a/tools/coder.py +++ b/tools/coder.py @@ -3,6 +3,19 @@ import subprocess import re import glob as glob_module +# Global state to track the agent's current working directory +# This ensures that subsequent bash commands run in the correct directory +CURRENT_WORKSPACE = os.getcwd() + +def get_current_workspace(): + global CURRENT_WORKSPACE + return CURRENT_WORKSPACE + +def set_current_workspace(path): + global CURRENT_WORKSPACE + CURRENT_WORKSPACE = path + os.chdir(path) + schema_read_file = { "type": "function", "function": { @@ -20,7 +33,9 @@ schema_read_file = { def read_file(path): try: - with open(path, 'r', encoding='utf-8', errors='replace') as f: + # Use absolute path if it's relative to CURRENT_WORKSPACE + full_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path + with open(full_path, 'r', encoding='utf-8', errors='replace') as f: lines = f.readlines() return ''.join(f"{i+1}: {line}" for i, line in enumerate(lines)) except Exception as e: @@ -44,9 +59,10 @@ schema_write_file = { def write_file(path, content): try: - with open(path, 'w', encoding='utf-8') as f: + full_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path + with open(full_path, 'w', encoding='utf-8') as f: f.write(content) - return f"Successfully wrote to {path}" + return f"Successfully wrote to {full_path}" except Exception as e: return f"Error writing file: {str(e)}" @@ -69,14 +85,15 @@ schema_edit_file = { def edit_file(path, old_string, new_string): try: - with open(path, 'r', encoding='utf-8', errors='replace') as f: + full_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path + with open(full_path, 'r', encoding='utf-8', errors='replace') as f: content = f.read() if old_string not in content: - return f"Error: old_string not found in {path}" + return f"Error: old_string not found in {full_path}" new_content = content.replace(old_string, new_string) - with open(path, 'w', encoding='utf-8') as f: + with open(full_path, 'w', encoding='utf-8') as f: f.write(new_content) - return f"Successfully edited {path}" + return f"Successfully edited {full_path}" except Exception as e: return f"Error editing file: {str(e)}" @@ -84,7 +101,7 @@ schema_run_bash = { "type": "function", "function": { "name" : "run_bash", - "description" : "Run a bash command. Returns stdout, stderr, and return code.", + "description" : "Run a bash command. Returns stdout, stderr, and return code. Commands are executed relative to the current workspace.", "parameters" : { "type" : "object", "properties" : { @@ -97,7 +114,9 @@ schema_run_bash = { def run_bash(command): try: - result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30) + # Prepend 'cd' to the command to ensure it runs in the correct workspace + full_command = f"cd {get_current_workspace()} && {command}" + result = subprocess.run(full_command, shell=True, capture_output=True, text=True, timeout=30) return f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}\nreturn code: {result.returncode}" except Exception as e: return f"Error running command: {str(e)}" @@ -121,12 +140,15 @@ schema_search_code = { def search_code(pattern, search_type, path="."): try: + # Resolve path relative to workspace + search_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path + if search_type == "glob": - files = glob_module.glob(f"{path}/**/{pattern}", recursive=True) + files = glob_module.glob(f"{search_path}/**/{pattern}", recursive=True) return "\n".join(files) if files else "No files found" elif search_type == "content": results = [] - for root, dirs, files in os.walk(path): + for root, dirs, files in os.walk(search_path): for file in files: file_path = os.path.join(root, file) try: @@ -162,8 +184,39 @@ schema_git_operation = { def git_operation(args): try: - result = subprocess.run(["git", *args], capture_output=True, text=True, timeout=10) + # Execute git command from the current workspace + full_command = ["git", *args] + result = subprocess.run(full_command, cwd=get_current_workspace(), capture_output=True, text=True, timeout=10) return f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}\nreturn code: {result.returncode}" except Exception as e: return f"Error running git command: {str(e)}" +schema_workspace_change = { + "type": "function", + "function": { + "name": "workspace_change", + "description": "Change the current working directory of the agent session. Supports tilde (~) expansion for the user home directory (/home/aji/).", + "parameters": { + "type": "object", + "properties": { + "path": {"type": "string", "description": "The target directory path to change to."} + }, + "required": ["path"] + } + } +} + +def workspace_change(path): + try: + if path.startswith("~"): + target_path = os.path.expanduser(path) + else: + target_path = os.path.abspath(path) + + if os.path.exists(target_path) and os.path.isdir(target_path): + set_current_workspace(target_path) + return f"Successfully changed workspace to: {target_path}" + else: + return f"Error: The path {target_path} does not exist or is not a directory." + except Exception as e: + return f"Error changing workspace: {str(e)}"