Tools: 터미널 AI 에이전트 구축 (v5) - Expert Insights
터미널 AI 에이전트 구축 (v5)
1. CLI AI 에이전트 생태계
Continue.dev
OpenCode
2. 로컬 LLM API 엔드포인트 설정
Ollama 설치
모델 다운로드 및 실행
API 테스트
3. 간단한 Python CLI 에이전트 구축
4. tmux와의 통합
tmux 자동화 스크립트
5. 커스텀 도구 개발
코드 검색 도구
Git 통합 도구
6. 컨텍스트 윈도우 관리 터미널 기반 AI 에이전트는 개발자에게 매우 실용적인 도구로 자리 잡았습니다. 다양한 CLI 기반 AI 도구들 중에서 가장 효율적인 방식으로 개발자 워크플로우를 개선할 수 있는 방법을 소개합니다. 현재 CLI AI 에이전트 시장은 다음과 같은 주요 도구들로 구성되어 있습니다: 이들 도구들은 모두 자체적으로 파일 편집, 코드 생성, git 통합 기능을 제공하지만, 커스터마이징과 성능 최적화에 있어 한계가 있습니다. 로컬 LLM을 사용하면 보안성과 성능을 높일 수 있습니다: 터미널 에이전트는 tmux와 함께 사용될 때 더욱 강력합니다: 대규모 코드베이스에서는 컨텍스트 윈도우 관리가 중요합니다: Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse
$ -weight: 500;">pip -weight: 500;">install aider
aider --help
-weight: 500;">pip -weight: 500;">install aider
aider --help
-weight: 500;">pip -weight: 500;">install aider
aider --help
-weight: 500;">npm -weight: 500;">install -g continue
-weight: 500;">npm -weight: 500;">install -g continue
-weight: 500;">npm -weight: 500;">install -g continue
-weight: 500;">git clone https://github.com/opencode-dev/opencode
cd opencode && -weight: 500;">pip -weight: 500;">install -e .
-weight: 500;">git clone https://github.com/opencode-dev/opencode
cd opencode && -weight: 500;">pip -weight: 500;">install -e .
-weight: 500;">git clone https://github.com/opencode-dev/opencode
cd opencode && -weight: 500;">pip -weight: 500;">install -e .
# Ubuntu/Debian
-weight: 500;">curl -fsSL https://ollama.com/-weight: 500;">install.sh | sh # macOS
-weight: 500;">brew -weight: 500;">install ollama # 시작
ollama serve
# Ubuntu/Debian
-weight: 500;">curl -fsSL https://ollama.com/-weight: 500;">install.sh | sh # macOS
-weight: 500;">brew -weight: 500;">install ollama # 시작
ollama serve
# Ubuntu/Debian
-weight: 500;">curl -fsSL https://ollama.com/-weight: 500;">install.sh | sh # macOS
-weight: 500;">brew -weight: 500;">install ollama # 시작
ollama serve
# llama3.2 모델 다운로드
ollama pull llama3.2 # 로컬 API 시작
ollama run llama3.2
# llama3.2 모델 다운로드
ollama pull llama3.2 # 로컬 API 시작
ollama run llama3.2
# llama3.2 모델 다운로드
ollama pull llama3.2 # 로컬 API 시작
ollama run llama3.2
# -weight: 500;">curl로 테스트
-weight: 500;">curl http://localhost:11434/api/generate \ -H "Content-Type: application/json" \ -d '{ "model": "llama3.2", "prompt": "Hello, how are you?", "stream": false }'
# -weight: 500;">curl로 테스트
-weight: 500;">curl http://localhost:11434/api/generate \ -H "Content-Type: application/json" \ -d '{ "model": "llama3.2", "prompt": "Hello, how are you?", "stream": false }'
# -weight: 500;">curl로 테스트
-weight: 500;">curl http://localhost:11434/api/generate \ -H "Content-Type: application/json" \ -d '{ "model": "llama3.2", "prompt": "Hello, how are you?", "stream": false }'
# aider_agent.py
import openai
import json
import subprocess
import os
from typing import List, Dict class TerminalAgent: def __init__(self, model="llama3.2"): self.model = model self.client = openai.OpenAI( base_url="http://localhost:11434/v1", api_key="ollama" ) self.context = [] def call_llm(self, prompt: str) -> str: response = self.client.chat.completions.create( model=self.model, messages=[{"role": "user", "content": prompt}], temperature=0.7 ) return response.choices[0].message.content def execute_command(self, command: str) -> str: try: result = subprocess.run( command, shell=True, capture_output=True, text=True ) return result.stdout + result.stderr except Exception as e: return f"Error: {str(e)}" def get_file_content(self, filepath: str) -> str: try: with open(filepath, 'r') as f: return f.read() except Exception as e: return f"Error reading file: {str(e)}" def save_file(self, filepath: str, content: str) -> bool: try: with open(filepath, 'w') as f: f.write(content) return True except Exception as e: print(f"Error saving file: {str(e)}") return False # 사용 예시
if __name__ == "__main__": agent = TerminalAgent() # 코드 생성 예시 prompt = """ Create a Python function that calculates the fibonacci sequence up to n terms. Return the list of numbers. """ response = agent.call_llm(prompt) print("Generated code:") print(response)
# aider_agent.py
import openai
import json
import subprocess
import os
from typing import List, Dict class TerminalAgent: def __init__(self, model="llama3.2"): self.model = model self.client = openai.OpenAI( base_url="http://localhost:11434/v1", api_key="ollama" ) self.context = [] def call_llm(self, prompt: str) -> str: response = self.client.chat.completions.create( model=self.model, messages=[{"role": "user", "content": prompt}], temperature=0.7 ) return response.choices[0].message.content def execute_command(self, command: str) -> str: try: result = subprocess.run( command, shell=True, capture_output=True, text=True ) return result.stdout + result.stderr except Exception as e: return f"Error: {str(e)}" def get_file_content(self, filepath: str) -> str: try: with open(filepath, 'r') as f: return f.read() except Exception as e: return f"Error reading file: {str(e)}" def save_file(self, filepath: str, content: str) -> bool: try: with open(filepath, 'w') as f: f.write(content) return True except Exception as e: print(f"Error saving file: {str(e)}") return False # 사용 예시
if __name__ == "__main__": agent = TerminalAgent() # 코드 생성 예시 prompt = """ Create a Python function that calculates the fibonacci sequence up to n terms. Return the list of numbers. """ response = agent.call_llm(prompt) print("Generated code:") print(response)
# aider_agent.py
import openai
import json
import subprocess
import os
from typing import List, Dict class TerminalAgent: def __init__(self, model="llama3.2"): self.model = model self.client = openai.OpenAI( base_url="http://localhost:11434/v1", api_key="ollama" ) self.context = [] def call_llm(self, prompt: str) -> str: response = self.client.chat.completions.create( model=self.model, messages=[{"role": "user", "content": prompt}], temperature=0.7 ) return response.choices[0].message.content def execute_command(self, command: str) -> str: try: result = subprocess.run( command, shell=True, capture_output=True, text=True ) return result.stdout + result.stderr except Exception as e: return f"Error: {str(e)}" def get_file_content(self, filepath: str) -> str: try: with open(filepath, 'r') as f: return f.read() except Exception as e: return f"Error reading file: {str(e)}" def save_file(self, filepath: str, content: str) -> bool: try: with open(filepath, 'w') as f: f.write(content) return True except Exception as e: print(f"Error saving file: {str(e)}") return False # 사용 예시
if __name__ == "__main__": agent = TerminalAgent() # 코드 생성 예시 prompt = """ Create a Python function that calculates the fibonacci sequence up to n terms. Return the list of numbers. """ response = agent.call_llm(prompt) print("Generated code:") print(response)
# tmux 세션 생성
tmux new-session -s ai_agent -d # 세션에 명령 실행
tmux send-keys -t ai_agent "python aider_agent.py" Enter # 세션에 파일 생성
tmux send-keys -t ai_agent "touch test.py" Enter # 세션 정보 확인
tmux list-sessions
# tmux 세션 생성
tmux new-session -s ai_agent -d # 세션에 명령 실행
tmux send-keys -t ai_agent "python aider_agent.py" Enter # 세션에 파일 생성
tmux send-keys -t ai_agent "touch test.py" Enter # 세션 정보 확인
tmux list-sessions
# tmux 세션 생성
tmux new-session -s ai_agent -d # 세션에 명령 실행
tmux send-keys -t ai_agent "python aider_agent.py" Enter # 세션에 파일 생성
tmux send-keys -t ai_agent "touch test.py" Enter # 세션 정보 확인
tmux list-sessions
#!/bin/bash
# setup_tmux.sh SESSION_NAME="ai_dev"
tmux new-session -s $SESSION_NAME -d # 메인 창 생성
tmux new-window -t $SESSION_NAME:1 -n "main"
tmux send-keys -t $SESSION_NAME:1 "cd ~/projects/myproject" Enter # 터미널 창 생성
tmux new-window -t $SESSION_NAME:2 -n "terminal"
tmux send-keys -t $SESSION_NAME:2 "python aider_agent.py" Enter # 에이전트 창 생성
tmux new-window -t $SESSION_NAME:3 -n "agent"
tmux send-keys -t $SESSION_NAME:3 "watch -n 1 'ls -la'" Enter # 세션에 연결
tmux attach -t $SESSION_NAME
#!/bin/bash
# setup_tmux.sh SESSION_NAME="ai_dev"
tmux new-session -s $SESSION_NAME -d # 메인 창 생성
tmux new-window -t $SESSION_NAME:1 -n "main"
tmux send-keys -t $SESSION_NAME:1 "cd ~/projects/myproject" Enter # 터미널 창 생성
tmux new-window -t $SESSION_NAME:2 -n "terminal"
tmux send-keys -t $SESSION_NAME:2 "python aider_agent.py" Enter # 에이전트 창 생성
tmux new-window -t $SESSION_NAME:3 -n "agent"
tmux send-keys -t $SESSION_NAME:3 "watch -n 1 'ls -la'" Enter # 세션에 연결
tmux attach -t $SESSION_NAME
#!/bin/bash
# setup_tmux.sh SESSION_NAME="ai_dev"
tmux new-session -s $SESSION_NAME -d # 메인 창 생성
tmux new-window -t $SESSION_NAME:1 -n "main"
tmux send-keys -t $SESSION_NAME:1 "cd ~/projects/myproject" Enter # 터미널 창 생성
tmux new-window -t $SESSION_NAME:2 -n "terminal"
tmux send-keys -t $SESSION_NAME:2 "python aider_agent.py" Enter # 에이전트 창 생성
tmux new-window -t $SESSION_NAME:3 -n "agent"
tmux send-keys -t $SESSION_NAME:3 "watch -n 1 'ls -la'" Enter # 세션에 연결
tmux attach -t $SESSION_NAME
# tools/code_search.py
import os
import re class CodeSearchTool: def __init__(self, project_root: str): self.project_root = project_root def search_in_files(self, pattern: str, file_extensions: list = None) -> List[Dict]: results = [] if file_extensions is None: file_extensions = ['.py', '.js', '.ts', '.java', '.cpp'] for root, dirs, files in os.walk(self.project_root): for file in files: if any(file.endswith(ext) for ext in file_extensions): filepath = os.path.join(root, file) try: with open(filepath, 'r') as f: content = f.read() matches = re.finditer(pattern, content) for match in matches: results.append({ 'file': filepath, 'line': content[:match.-weight: 500;">start()].count('\n') + 1, 'context': self.get_context(content, match.-weight: 500;">start()) }) except Exception as e: continue return results def get_context(self, content: str, position: int, context_lines: int = 3) -> str: lines = content.split('\n') line_num = content[:position].count('\n') -weight: 500;">start = max(0, line_num - context_lines) end = min(len(lines), line_num + context_lines + 1) return '\n'.join(lines[-weight: 500;">start:end]) # 사용 예시
searcher = CodeSearchTool("/path/to/project")
results = searcher.search_in_files(r"def.*function_name")
# tools/code_search.py
import os
import re class CodeSearchTool: def __init__(self, project_root: str): self.project_root = project_root def search_in_files(self, pattern: str, file_extensions: list = None) -> List[Dict]: results = [] if file_extensions is None: file_extensions = ['.py', '.js', '.ts', '.java', '.cpp'] for root, dirs, files in os.walk(self.project_root): for file in files: if any(file.endswith(ext) for ext in file_extensions): filepath = os.path.join(root, file) try: with open(filepath, 'r') as f: content = f.read() matches = re.finditer(pattern, content) for match in matches: results.append({ 'file': filepath, 'line': content[:match.-weight: 500;">start()].count('\n') + 1, 'context': self.get_context(content, match.-weight: 500;">start()) }) except Exception as e: continue return results def get_context(self, content: str, position: int, context_lines: int = 3) -> str: lines = content.split('\n') line_num = content[:position].count('\n') -weight: 500;">start = max(0, line_num - context_lines) end = min(len(lines), line_num + context_lines + 1) return '\n'.join(lines[-weight: 500;">start:end]) # 사용 예시
searcher = CodeSearchTool("/path/to/project")
results = searcher.search_in_files(r"def.*function_name")
# tools/code_search.py
import os
import re class CodeSearchTool: def __init__(self, project_root: str): self.project_root = project_root def search_in_files(self, pattern: str, file_extensions: list = None) -> List[Dict]: results = [] if file_extensions is None: file_extensions = ['.py', '.js', '.ts', '.java', '.cpp'] for root, dirs, files in os.walk(self.project_root): for file in files: if any(file.endswith(ext) for ext in file_extensions): filepath = os.path.join(root, file) try: with open(filepath, 'r') as f: content = f.read() matches = re.finditer(pattern, content) for match in matches: results.append({ 'file': filepath, 'line': content[:match.-weight: 500;">start()].count('\n') + 1, 'context': self.get_context(content, match.-weight: 500;">start()) }) except Exception as e: continue return results def get_context(self, content: str, position: int, context_lines: int = 3) -> str: lines = content.split('\n') line_num = content[:position].count('\n') -weight: 500;">start = max(0, line_num - context_lines) end = min(len(lines), line_num + context_lines + 1) return '\n'.join(lines[-weight: 500;">start:end]) # 사용 예시
searcher = CodeSearchTool("/path/to/project")
results = searcher.search_in_files(r"def.*function_name")
# tools/git_tool.py
import subprocess
import json class GitTool: def __init__(self): pass def get_status(self) -> Dict: result = subprocess.run(['-weight: 500;">git', '-weight: 500;">status', '--porcelain'], capture_output=True, text=True) return {"-weight: 500;">status": result.stdout.strip()} def get_diff(self) -> str: result = subprocess.run(['-weight: 500;">git', 'diff'], capture_output=True, text=True) return result.stdout def commit_changes(self, message: str) -> bool: try: subprocess.run(['-weight: 500;">git', 'add', '.'], check=True) subprocess.run(['-weight: 500;">git', 'commit', '-m', message], check=True) return True except subprocess.CalledProcessError: return False def get_branch_info(self) -> Dict: result = subprocess.run(['-weight: 500;">git', 'branch', '--show-current'], capture_output=True, text=True) return {"branch": result.stdout.strip()} # 사용 예시
git_tool = GitTool()
-weight: 500;">status = git_tool.get_status()
print(json.dumps(-weight: 500;">status, indent=2))
# tools/git_tool.py
import subprocess
import json class GitTool: def __init__(self): pass def get_status(self) -> Dict: result = subprocess.run(['-weight: 500;">git', '-weight: 500;">status', '--porcelain'], capture_output=True, text=True) return {"-weight: 500;">status": result.stdout.strip()} def get_diff(self) -> str: result = subprocess.run(['-weight: 500;">git', 'diff'], capture_output=True, text=True) return result.stdout def commit_changes(self, message: str) -> bool: try: subprocess.run(['-weight: 500;">git', 'add', '.'], check=True) subprocess.run(['-weight: 500;">git', 'commit', '-m', message], check=True) return True except subprocess.CalledProcessError: return False def get_branch_info(self) -> Dict: result = subprocess.run(['-weight: 500;">git', 'branch', '--show-current'], capture_output=True, text=True) return {"branch": result.stdout.strip()} # 사용 예시
git_tool = GitTool()
-weight: 500;">status = git_tool.get_status()
print(json.dumps(-weight: 500;">status, indent=2))
# tools/git_tool.py
import subprocess
import json class GitTool: def __init__(self): pass def get_status(self) -> Dict: result = subprocess.run(['-weight: 500;">git', '-weight: 500;">status', '--porcelain'], capture_output=True, text=True) return {"-weight: 500;">status": result.stdout.strip()} def get_diff(self) -> str: result = subprocess.run(['-weight: 500;">git', 'diff'], capture_output=True, text=True) return result.stdout def commit_changes(self, message: str) -> bool: try: subprocess.run(['-weight: 500;">git', 'add', '.'], check=True) subprocess.run(['-weight: 500;">git', 'commit', '-m', message], check=True) return True except subprocess.CalledProcessError: return False def get_branch_info(self) -> Dict: result = subprocess.run(['-weight: 500;">git', 'branch', '--show-current'], capture_output=True, text=True) return {"branch": result.stdout.strip()} # 사용 예시
git_tool = GitTool()
-weight: 500;">status = git_tool.get_status()
print(json.dumps(-weight: 500;">status, indent=2))
python
# context_manager.py
import tiktoken
from typing import List, Dict class ContextManager: def __init__(self, model_name: str = "gpt-4"): self.encoding = tiktoken.encoding_for_model(model_name) self.max_tokens = 8192 # 8K 토큰 제한 def count_tokens(self, text: str) -> int: return len(self.encoding.encode(text)) def truncate_context(self, context: List[Dict], max_tokens: int = None) -> List[Dict]: if max_tokens is None: max_tokens = self.max_tokens truncated = [] current_tokens = 0 # 최근 항목부터 처리 for item in reversed(context): item_tokens = self.count_tokens(str(item)) if current_tokens + item_tokens <= max_tokens: truncated.append(item) current_tokens += item_tokens else: break return list(reversed(truncated)) def add_context --- 📥 **Get the full guide on Gumroad**: https://gumroad.com/l/auto ($5)
python
# context_manager.py
import tiktoken
from typing import List, Dict class ContextManager: def __init__(self, model_name: str = "gpt-4"): self.encoding = tiktoken.encoding_for_model(model_name) self.max_tokens = 8192 # 8K 토큰 제한 def count_tokens(self, text: str) -> int: return len(self.encoding.encode(text)) def truncate_context(self, context: List[Dict], max_tokens: int = None) -> List[Dict]: if max_tokens is None: max_tokens = self.max_tokens truncated = [] current_tokens = 0 # 최근 항목부터 처리 for item in reversed(context): item_tokens = self.count_tokens(str(item)) if current_tokens + item_tokens <= max_tokens: truncated.append(item) current_tokens += item_tokens else: break return list(reversed(truncated)) def add_context --- 📥 **Get the full guide on Gumroad**: https://gumroad.com/l/auto ($5)
python
# context_manager.py
import tiktoken
from typing import List, Dict class ContextManager: def __init__(self, model_name: str = "gpt-4"): self.encoding = tiktoken.encoding_for_model(model_name) self.max_tokens = 8192 # 8K 토큰 제한 def count_tokens(self, text: str) -> int: return len(self.encoding.encode(text)) def truncate_context(self, context: List[Dict], max_tokens: int = None) -> List[Dict]: if max_tokens is None: max_tokens = self.max_tokens truncated = [] current_tokens = 0 # 최근 항목부터 처리 for item in reversed(context): item_tokens = self.count_tokens(str(item)) if current_tokens + item_tokens <= max_tokens: truncated.append(item) current_tokens += item_tokens else: break return list(reversed(truncated)) def add_context --- 📥 **Get the full guide on Gumroad**: https://gumroad.com/l/auto ($5)