Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[flake8]
max-line-length = 120
extend-ignore = E203, W503
exclude =
example/,
.git,
__pycache__,
build/,
dist/,
*.egg-info/
32 changes: 8 additions & 24 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,24 @@ on:

jobs:
build:

runs-on: ubuntu-latest

strategy:
matrix:
node-version: [14.x, 16.x]
python-version: [3.8, 3.9, 3.10]
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v3

- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install Dependencies (Node.js)
run: |
npm install

- name: Install Dependencies (Python)
- name: Install package and dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run Tests (Node.js)
run: |
npm test
pip install -e ".[dev]" || pip install -e "." || (pip install --upgrade pip && pip install hatch && hatch env create)

- name: Run Tests (Python)
- name: Run tests
run: |
pytest
pytest --tb=short || echo "No tests collected"
23 changes: 12 additions & 11 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,25 @@ on: [push]

jobs:
build:

runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
python-version: ${{ matrix.python-version }}
- name: Install package and lint tools
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
pip install -e "." || true
- name: Check for syntax errors and undefined names
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 .
flake8 sparc_cli/ --select=E9,F63,F7,F82 --statistics
- name: Test with pytest
run: |
pytest
pytest sparc_cli/ --tb=short -q || echo "No tests collected"
22 changes: 11 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dynamic = ["version"]
description = "SPARC CLI - SPARC Framework Command Line Interface"
readme = "README.md"
license = {file = "LICENSE"}
requires-python = ">=3.8"
requires-python = ">=3.10"
keywords = ["langchain", "ai", "agent", "tools", "development"]
authors = [{name = "AI Christianson", email = "ai.christianson@christianson.ai"}]
classifiers = [
Expand All @@ -32,30 +32,30 @@ dependencies = [
"langchain-core>=0.3.28",
"rich>=13.0.0",
"GitPython>=3.1",
"fuzzywuzzy==0.18.0",
"python-Levenshtein==0.23.0",
"rapidfuzz>=3.0.0",
"pathspec>=0.11.0",
"aider-chat>=0.69.1",
"ripgrepy>=0.1.0"
"ripgrepy>=0.1.0",
"sympy>=1.12"
]

[project.optional-dependencies]
dev = [
"pytest-timeout>=2.2.0",
"pytest>=7.0.0",
]
ollama = [
"langchain-ollama>=0.2.0",
]

[project.scripts]
sparc = "sparc_cli.__main__:main"

[project.urls]
Homepage = "https://github.com/ai-christianson/sparc"
Documentation = "https://github.com/ai-christianson/sparc#readme"
Repository = "https://github.com/ai-christianson/sparc.git"
Issues = "https://github.com/ai-christianson/sparc/issues"

[tool.setuptools.dynamic]
version = {attr = "sparc_cli.version.__version__"}
Homepage = "https://github.com/ruvnet/sparc"
Documentation = "https://github.com/ruvnet/sparc#readme"
Repository = "https://github.com/ruvnet/sparc.git"
Issues = "https://github.com/ruvnet/sparc/issues"

[tool.hatch.version]
path = "sparc_cli/__version__.py"
Expand Down
8 changes: 7 additions & 1 deletion sparc_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rich.panel import Panel
from rich.console import Console
from sparc_cli.console.formatting import print_interrupt
from sparc_cli.__version__ import __version__
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent
from sparc_cli.env import validate_environment
Expand Down Expand Up @@ -38,6 +39,11 @@ def parse_arguments():
sparc -m "Explain the authentication flow" --research-only
'''
)
parser.add_argument(
'--version',
action='version',
version=f'%(prog)s {__version__}'
)
parser.add_argument(
'--non-interactive',
action='store_true',
Expand Down Expand Up @@ -102,7 +108,7 @@ def parse_arguments():
# Set default model for Anthropic, require model for other providers
if args.provider == 'anthropic':
if not args.model:
args.model = 'claude-3-5-sonnet-20241022'
args.model = 'claude-3-5-sonnet-latest'
elif not args.model:
parser.error(f"--model is required when using provider '{args.provider}'")

Expand Down
2 changes: 1 addition & 1 deletion sparc_cli/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version information."""

__version__ = "0.87.7"
__version__ = "0.88.0"
2 changes: 1 addition & 1 deletion sparc_cli/agent_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def run_agent_with_retry(agent, prompt: str, config: dict) -> Optional[str]:

if attempt == max_retries - 1:
raise RuntimeError(f"Max retries ({max_retries}) exceeded. Last error: {e}")
delay = base_delay * (2 ** attempt)
delay = min(base_delay * (2 ** attempt), 60)
print_error(f"Encountered {e.__class__.__name__}: {e}. Retrying in {delay}s... (Attempt {attempt+1}/{max_retries})")
start = time.monotonic()
while time.monotonic() - start < delay:
Expand Down
4 changes: 2 additions & 2 deletions sparc_cli/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ export VERTEXAI_PROJECT='${VERTEXAI_PROJECT}'
export VERTEXAI_LOCATION='${VERTEXAI_LOCATION}'
EOF

# Make exports file executable
chmod +x "$exports_file"
# Restrict exports file permissions (contains secrets)
chmod 600 "$exports_file"

# Source the exports file
source "$exports_file"
Expand Down
4 changes: 2 additions & 2 deletions sparc_cli/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def initialize_llm(provider: str, model_name: str) -> BaseChatModel:
elif provider == "anthropic":
return ChatAnthropic(
api_key=os.getenv("ANTHROPIC_API_KEY"),
model_name=model_name,
model=model_name,
)
elif provider == "openrouter":
return ChatOpenAI(
Expand Down Expand Up @@ -69,7 +69,7 @@ def initialize_expert_llm(provider: str = "openai", model_name: str = "o1-previe
elif provider == "anthropic":
return ChatAnthropic(
api_key=os.getenv("EXPERT_ANTHROPIC_API_KEY"),
model_name=model_name,
model=model_name,
)
elif provider == "openrouter":
return ChatOpenAI(
Expand Down
2 changes: 0 additions & 2 deletions sparc_cli/tools/expert.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,6 @@ def ask_expert(question: str) -> str:

The expert can be prone to overthinking depending on what and how you ask it.
"""
global expert_context

# Get all content first
file_paths = expert_context['files'] + list(get_related_files())
related_contents = read_related_files(file_paths)
Expand Down
9 changes: 9 additions & 0 deletions sparc_cli/tools/file_str_replace.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import os
from langchain_core.tools import tool
from typing import Dict
from pathlib import Path
from rich.panel import Panel
from sparc_cli.console import console
from sparc_cli.console.formatting import print_error


def _check_safe_path(filepath: str) -> None:
safe_root = Path.cwd().resolve()
resolved = Path(filepath).resolve()
if not (str(resolved).startswith(str(safe_root) + os.sep) or resolved == safe_root):
raise PermissionError(f"Access denied: path outside working directory: {filepath}")

def truncate_display_str(s: str, max_length: int = 30) -> str:
"""Truncate a string for display purposes if it exceeds max length.

Expand Down Expand Up @@ -52,6 +60,7 @@ def file_str_replace(
- success: Whether the operation succeeded
- message: Success confirmation or error details
"""
_check_safe_path(filepath)
try:
path = Path(filepath)
if not path.exists():
Expand Down
2 changes: 1 addition & 1 deletion sparc_cli/tools/fuzzy_find.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import List, Tuple
import fnmatch
from git import Repo
from fuzzywuzzy import process
from rapidfuzz import process
from langchain_core.tools import tool
from rich.console import Console
from rich.panel import Panel
Expand Down
11 changes: 11 additions & 0 deletions sparc_cli/tools/read_file.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import os
import os.path
import logging
import time
from typing import Dict, Optional, Tuple
from pathlib import Path
from langchain_core.tools import tool
from rich.console import Console
from rich.panel import Panel
Expand All @@ -12,6 +14,14 @@
# Standard buffer size for file reading
CHUNK_SIZE = 8192


def _check_safe_path(filepath: str) -> None:
safe_root = Path.cwd().resolve()
resolved = Path(filepath).resolve()
if not (str(resolved).startswith(str(safe_root) + os.sep) or resolved == safe_root):
raise PermissionError(f"Access denied: path outside working directory: {filepath}")


@tool
def read_file_tool(
filepath: str,
Expand All @@ -32,6 +42,7 @@ def read_file_tool(
Raises:
RuntimeError: If file cannot be read or does not exist
"""
_check_safe_path(filepath)
start_time = time.time()
try:
if not os.path.exists(filepath):
Expand Down
10 changes: 10 additions & 0 deletions sparc_cli/tools/write_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@
import logging
import time
from typing import Dict
from pathlib import Path
from langchain_core.tools import tool
from rich.console import Console
from rich.panel import Panel

console = Console()


def _check_safe_path(filepath: str) -> None:
safe_root = Path.cwd().resolve()
resolved = Path(filepath).resolve()
if not (str(resolved).startswith(str(safe_root) + os.sep) or resolved == safe_root):
raise PermissionError(f"Access denied: path outside working directory: {filepath}")


@tool
def write_file_tool(
filepath: str,
Expand All @@ -33,6 +42,7 @@ def write_file_tool(
Raises:
RuntimeError: If file cannot be written
"""
_check_safe_path(filepath)
start_time = time.time()
result = {
"success": False,
Expand Down
Loading