mini-swe-agent 1.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.
Files changed (47) hide show
  1. mini_swe_agent-1.1.0.dist-info/METADATA +288 -0
  2. mini_swe_agent-1.1.0.dist-info/RECORD +47 -0
  3. mini_swe_agent-1.1.0.dist-info/WHEEL +5 -0
  4. mini_swe_agent-1.1.0.dist-info/entry_points.txt +5 -0
  5. mini_swe_agent-1.1.0.dist-info/licenses/LICENSE.md +21 -0
  6. mini_swe_agent-1.1.0.dist-info/top_level.txt +1 -0
  7. minisweagent/__init__.py +67 -0
  8. minisweagent/__main__.py +7 -0
  9. minisweagent/agents/__init__.py +1 -0
  10. minisweagent/agents/default.py +129 -0
  11. minisweagent/agents/interactive.py +148 -0
  12. minisweagent/agents/interactive_textual.py +324 -0
  13. minisweagent/config/README.md +9 -0
  14. minisweagent/config/__init__.py +24 -0
  15. minisweagent/config/__pycache__/__init__.cpython-313.pyc +0 -0
  16. minisweagent/config/default.yaml +143 -0
  17. minisweagent/config/extra/__init__.py +1 -0
  18. minisweagent/config/extra/swebench.yaml +229 -0
  19. minisweagent/config/github_issue.yaml +146 -0
  20. minisweagent/config/local.yaml +154 -0
  21. minisweagent/config/local2.tcss +128 -0
  22. minisweagent/environments/__init__.py +1 -0
  23. minisweagent/environments/docker.py +98 -0
  24. minisweagent/environments/extra/__init__.py +0 -0
  25. minisweagent/environments/extra/swerex_docker.py +39 -0
  26. minisweagent/environments/local.py +33 -0
  27. minisweagent/environments/singularity.py +52 -0
  28. minisweagent/models/__init__.py +81 -0
  29. minisweagent/models/anthropic.py +19 -0
  30. minisweagent/models/litellm_model.py +64 -0
  31. minisweagent/models/test_models.py +38 -0
  32. minisweagent/models/utils/cache_control.py +42 -0
  33. minisweagent/models/utils/key_per_thread.py +18 -0
  34. minisweagent/py.typed +0 -0
  35. minisweagent/run/__init__.py +1 -0
  36. minisweagent/run/extra/__init__.py +0 -0
  37. minisweagent/run/extra/config.py +100 -0
  38. minisweagent/run/extra/swebench.py +235 -0
  39. minisweagent/run/extra/swebench_single.py +53 -0
  40. minisweagent/run/extra/utils/batch_progress.py +164 -0
  41. minisweagent/run/github_issue.py +80 -0
  42. minisweagent/run/hello_world.py +36 -0
  43. minisweagent/run/inspector.py +212 -0
  44. minisweagent/run/mini.py +118 -0
  45. minisweagent/run/mini_extra.py +44 -0
  46. minisweagent/run/utils/__init__.py +0 -0
  47. minisweagent/run/utils/save.py +35 -0
@@ -0,0 +1,98 @@
1
+ import os
2
+ import shlex
3
+ import subprocess
4
+ import uuid
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class DockerEnvironmentConfig:
11
+ image: str
12
+ cwd: str = "/"
13
+ """Working directory in which to execute commands."""
14
+ env: dict[str, str] = field(default_factory=dict)
15
+ """Environment variables to set in the container."""
16
+ forward_env: list[str] = field(default_factory=list)
17
+ """Environment variables to forward to the container.
18
+ Variables are only forwarded if they are set in the host environment.
19
+ In case of conflict with `env`, the `env` variables take precedence.
20
+ """
21
+ timeout: int = 30
22
+ """Timeout for executing commands in the container."""
23
+ executable: str = "docker"
24
+ """Path to the docker/container executable."""
25
+ run_args: list[str] = field(default_factory=list)
26
+ """Additional arguments to pass to the docker/container executable."""
27
+
28
+
29
+ class DockerEnvironment:
30
+ def __init__(self, *, config_class: type = DockerEnvironmentConfig, **kwargs):
31
+ """This class executes bash commands in a Docker container using direct docker commands.
32
+ See `DockerEnvironmentConfig` for keyword arguments.
33
+ """
34
+ self.container_id: str | None = None
35
+ self.config = config_class(**kwargs)
36
+ self._start_container()
37
+
38
+ def _start_container(self):
39
+ """Start the Docker container and return the container ID."""
40
+ container_name = f"minisweagent-{uuid.uuid4().hex[:8]}"
41
+ cmd = [
42
+ self.config.executable,
43
+ "run",
44
+ "-d",
45
+ "--name",
46
+ container_name,
47
+ "-w",
48
+ self.config.cwd,
49
+ *self.config.run_args,
50
+ self.config.image,
51
+ "sleep",
52
+ "infinity", # Keep container running
53
+ ]
54
+ print(f"Starting container with command: {shlex.join(cmd)}")
55
+ result = subprocess.run(
56
+ cmd,
57
+ capture_output=True,
58
+ text=True,
59
+ timeout=120, # docker pull might take a while
60
+ check=True,
61
+ )
62
+ print(f"Started container {container_name} with ID {result.stdout.strip()}")
63
+ self.container_id = result.stdout.strip()
64
+
65
+ def execute(self, command: str, cwd: str = "") -> dict[str, Any]:
66
+ """Execute a command in the Docker container and return the result as a dict."""
67
+ cwd = cwd or self.config.cwd
68
+ assert self.container_id, "Container not started"
69
+
70
+ cmd = [self.config.executable, "exec", "-w", cwd]
71
+ for key in self.config.forward_env:
72
+ if (value := os.getenv(key)) is not None:
73
+ cmd.extend(["-e", f"{key}={value}"])
74
+ for key, value in self.config.env.items():
75
+ cmd.extend(["-e", f"{key}={value}"])
76
+ cmd.extend([self.container_id, "bash", "-lc", command])
77
+
78
+ result = subprocess.run(
79
+ cmd,
80
+ text=True,
81
+ timeout=self.config.timeout,
82
+ encoding="utf-8",
83
+ errors="replace",
84
+ stdout=subprocess.PIPE,
85
+ stderr=subprocess.STDOUT,
86
+ )
87
+ return {"output": result.stdout, "returncode": result.returncode}
88
+
89
+ def cleanup(self):
90
+ """Stop and remove the Docker container."""
91
+ if getattr(self, "container_id", None) is not None: # if init fails early, container_id might not be set
92
+ print(f"Stopping container {self.container_id}")
93
+ cmd = f"(timeout 60 {self.config.executable} stop {self.container_id} || {self.config.executable} rm -f {self.container_id}) >/dev/null 2>&1 &"
94
+ subprocess.Popen(cmd, shell=True)
95
+
96
+ def __del__(self):
97
+ """Cleanup container when object is destroyed."""
98
+ self.cleanup()
File without changes
@@ -0,0 +1,39 @@
1
+ import asyncio
2
+ from dataclasses import dataclass, field
3
+ from typing import Any
4
+
5
+ from swerex.deployment.docker import DockerDeployment
6
+ from swerex.runtime.abstract import Command as RexCommand
7
+
8
+
9
+ @dataclass
10
+ class SwerexDockerEnvironmentConfig:
11
+ image: str
12
+ cwd: str = "/"
13
+ """Working directory in which to execute commands."""
14
+ timeout: int = 30
15
+ """Timeout for executing commands in the container."""
16
+ deployment_extra_kwargs: dict[str, Any] = field(default_factory=dict)
17
+ """Extra kwargs to pass to DockerDeployment."""
18
+
19
+
20
+ class SwerexDockerEnvironment:
21
+ def __init__(self, **kwargs):
22
+ """This class executes bash commands in a Docker container using SWE-ReX for sandboxing."""
23
+ self.config = SwerexDockerEnvironmentConfig(**kwargs)
24
+ self.deployment = DockerDeployment(image=self.config.image, **self.config.deployment_extra_kwargs)
25
+ asyncio.run(self.deployment.start())
26
+
27
+ def execute(self, command: str, cwd: str = "") -> dict[str, Any]:
28
+ """Execute a command in the environment and return the raw output."""
29
+ output = asyncio.run(
30
+ self.deployment.runtime.execute(
31
+ RexCommand(
32
+ command=command, shell=True, check=False, cwd=cwd or self.config.cwd, timeout=self.config.timeout
33
+ )
34
+ )
35
+ )
36
+ return {
37
+ "output": f"<stdout>\n{output.stdout}</stdout>\n<stderr>\n{output.stderr}</stderr>",
38
+ "returncode": output.exit_code,
39
+ }
@@ -0,0 +1,33 @@
1
+ import os
2
+ import subprocess
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass
7
+ class LocalEnvironmentConfig:
8
+ cwd: str = ""
9
+ env: dict[str, str] = field(default_factory=dict)
10
+ timeout: int = 30
11
+
12
+
13
+ class LocalEnvironment:
14
+ def __init__(self, *, config_class: type = LocalEnvironmentConfig, **kwargs):
15
+ """This class executes bash commands directly on the local machine."""
16
+ self.config = config_class(**kwargs)
17
+
18
+ def execute(self, command: str, cwd: str = ""):
19
+ """Execute a command in the local environment and return the result as a dict."""
20
+ cwd = cwd or self.config.cwd or os.getcwd()
21
+ result = subprocess.run(
22
+ command,
23
+ shell=True,
24
+ text=True,
25
+ cwd=cwd,
26
+ env=os.environ | self.config.env,
27
+ timeout=self.config.timeout,
28
+ encoding="utf-8",
29
+ errors="replace",
30
+ stdout=subprocess.PIPE,
31
+ stderr=subprocess.STDOUT,
32
+ )
33
+ return {"output": result.stdout, "returncode": result.returncode}
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import os
4
+ import subprocess
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class SingularityEnvironmentConfig:
11
+ image: str
12
+ cwd: str = "/"
13
+ env: dict[str, str] = field(default_factory=dict)
14
+ """Environment variables to set in the container."""
15
+ forward_env: list[str] = field(default_factory=list)
16
+ """Environment variables to forward to the container."""
17
+ timeout: int = 30
18
+ """Timeout for executing commands in the container."""
19
+ executable: str = "singularity"
20
+ """Path to the singularity executable."""
21
+
22
+
23
+ class SingularityEnvironment:
24
+ def __init__(self, **kwargs):
25
+ """Singularity environment. See `SingularityEnvironmentConfig` for kwargs."""
26
+ self.config = SingularityEnvironmentConfig(**kwargs)
27
+
28
+ def execute(self, command: str, cwd: str = "") -> dict[str, Any]:
29
+ """Execute a command in a Singularity container and return the result as a dict."""
30
+ cmd = [self.config.executable, "exec"]
31
+
32
+ work_dir = cwd or self.config.cwd
33
+ if work_dir and work_dir != "/":
34
+ cmd.extend(["--pwd", work_dir])
35
+
36
+ for key in self.config.forward_env:
37
+ if (value := os.getenv(key)) is not None:
38
+ cmd.extend(["--env", f"{key}={value}"])
39
+ for key, value in self.config.env.items():
40
+ cmd.extend(["--env", f"{key}={value}"])
41
+
42
+ cmd.extend([self.config.image, "bash", "-c", command])
43
+ result = subprocess.run(
44
+ cmd,
45
+ text=True,
46
+ timeout=self.config.timeout,
47
+ encoding="utf-8",
48
+ errors="replace",
49
+ stdout=subprocess.PIPE,
50
+ stderr=subprocess.STDOUT,
51
+ )
52
+ return {"output": result.stdout, "returncode": result.returncode}
@@ -0,0 +1,81 @@
1
+ """This file provides convenience functions for selecting models.
2
+ You can ignore this file completely if you explicitly set your model in your run script.
3
+ """
4
+
5
+ import copy
6
+ import os
7
+ import threading
8
+
9
+ from minisweagent import Model
10
+
11
+
12
+ class GlobalModelStats:
13
+ """Global model statistics tracker with optional limits."""
14
+
15
+ def __init__(self):
16
+ self._cost = 0.0
17
+ self._n_calls = 0
18
+ self._lock = threading.Lock()
19
+ self.cost_limit = float(os.getenv("MSWEA_GLOBAL_COST_LIMIT", "0"))
20
+ self.call_limit = int(os.getenv("MSWEA_GLOBAL_CALL_LIMIT", "0"))
21
+ if (self.cost_limit > 0 or self.call_limit > 0) and not os.getenv("MINI_SWE_AGENT_SILENT_STARTUP"):
22
+ print(f"Global cost/call limit: ${self.cost_limit:.4f} / {self.call_limit}")
23
+
24
+ def add(self, cost: float) -> None:
25
+ """Add a model call with its cost, checking limits."""
26
+ with self._lock:
27
+ self._cost += cost
28
+ self._n_calls += 1
29
+ if 0 < self.cost_limit < self._cost or 0 < self.call_limit < self._n_calls + 1:
30
+ raise RuntimeError(f"Global cost/call limit exceeded: ${self._cost:.4f} / {self._n_calls + 1}")
31
+
32
+ @property
33
+ def cost(self) -> float:
34
+ return self._cost
35
+
36
+ @property
37
+ def n_calls(self) -> int:
38
+ return self._n_calls
39
+
40
+
41
+ GLOBAL_MODEL_STATS = GlobalModelStats()
42
+
43
+
44
+ def get_model(input_model_name: str | None = None, config: dict | None = None) -> Model:
45
+ """Get an initialized model object from any kind of user input or settings."""
46
+ resolved_model_name = get_model_name(input_model_name, config)
47
+ if config is None:
48
+ config = {}
49
+ config = copy.deepcopy(config)
50
+ config["model_name"] = resolved_model_name
51
+
52
+ # API key resolution (from env -> config -> None)
53
+ if "model_kwargs" not in config:
54
+ config["model_kwargs"] = {}
55
+ if from_env := os.getenv("MSWEA_MODEL_API_KEY"):
56
+ config["model_kwargs"]["api_key"] = from_env
57
+ return get_model_class(resolved_model_name)(**config)
58
+
59
+
60
+ def get_model_name(input_model_name: str | None = None, config: dict | None = None) -> str:
61
+ """Get a model name from any kind of user input or settings."""
62
+ if config is None:
63
+ config = {}
64
+ if input_model_name:
65
+ return input_model_name
66
+ if from_env := os.getenv("MSWEA_MODEL_NAME"):
67
+ return from_env
68
+ if from_config := config.get("model_name"):
69
+ return from_config
70
+ raise ValueError("No default model set. Please run `mini-extra config setup` to set one.")
71
+
72
+
73
+ def get_model_class(model_name: str) -> type:
74
+ """Select the best model class for a given model name."""
75
+ if any(s in model_name.lower() for s in ["anthropic", "sonnet", "opus", "claude"]):
76
+ from minisweagent.models.anthropic import AnthropicModel
77
+
78
+ return AnthropicModel
79
+ from minisweagent.models.litellm_model import LitellmModel
80
+
81
+ return LitellmModel
@@ -0,0 +1,19 @@
1
+ import os
2
+
3
+ from minisweagent.models.litellm_model import LitellmModel
4
+ from minisweagent.models.utils.cache_control import set_cache_control
5
+ from minisweagent.models.utils.key_per_thread import get_key_per_thread
6
+
7
+
8
+ class AnthropicModel(LitellmModel):
9
+ """For the use of anthropic models, we need to add explicit cache control marks
10
+ to the messages or we lose out on the benefits of the cache.
11
+ Because break points are limited per key, we also need to rotate between different keys
12
+ if running with multiple agents in parallel threads.
13
+ """
14
+
15
+ def query(self, messages: list[dict], **kwargs) -> dict:
16
+ api_key = None
17
+ if rotating_keys := os.getenv("ANTHROPIC_API_KEYS"):
18
+ api_key = get_key_per_thread(rotating_keys.split("::"))
19
+ return super().query(set_cache_control(messages), api_key=api_key, **kwargs)
@@ -0,0 +1,64 @@
1
+ import logging
2
+ from dataclasses import dataclass, field
3
+ from typing import Any
4
+
5
+ import litellm
6
+ from tenacity import (
7
+ before_sleep_log,
8
+ retry,
9
+ retry_if_not_exception_type,
10
+ stop_after_attempt,
11
+ wait_exponential,
12
+ )
13
+
14
+ from minisweagent.models import GLOBAL_MODEL_STATS
15
+
16
+ logger = logging.getLogger("litellm_model")
17
+
18
+
19
+ @dataclass
20
+ class LitellmModelConfig:
21
+ model_name: str
22
+ model_kwargs: dict[str, Any] = field(default_factory=dict)
23
+
24
+
25
+ class LitellmModel:
26
+ def __init__(self, **kwargs):
27
+ self.config = LitellmModelConfig(**kwargs)
28
+ self.cost = 0.0
29
+ self.n_calls = 0
30
+
31
+ @retry(
32
+ stop=stop_after_attempt(10),
33
+ wait=wait_exponential(multiplier=1, min=4, max=60),
34
+ before_sleep=before_sleep_log(logger, logging.WARNING),
35
+ retry=retry_if_not_exception_type(
36
+ (
37
+ litellm.exceptions.UnsupportedParamsError,
38
+ litellm.exceptions.NotFoundError,
39
+ litellm.exceptions.PermissionDeniedError,
40
+ litellm.exceptions.ContextWindowExceededError,
41
+ litellm.exceptions.APIError,
42
+ litellm.exceptions.AuthenticationError,
43
+ KeyboardInterrupt,
44
+ )
45
+ ),
46
+ )
47
+ def _query(self, messages: list[dict[str, str]], **kwargs):
48
+ try:
49
+ return litellm.completion(
50
+ model=self.config.model_name, messages=messages, **(self.config.model_kwargs | kwargs)
51
+ )
52
+ except litellm.exceptions.AuthenticationError as e:
53
+ e.message += " You can permanently set your API key with `mini-extra config set KEY VALUE`."
54
+ raise e
55
+
56
+ def query(self, messages: list[dict[str, str]], **kwargs) -> dict:
57
+ response = self._query(messages, **kwargs)
58
+ cost = litellm.cost_calculator.completion_cost(response)
59
+ self.n_calls += 1
60
+ self.cost += cost
61
+ GLOBAL_MODEL_STATS.add(cost)
62
+ return {
63
+ "content": response.choices[0].message.content or "", # type: ignore
64
+ }
@@ -0,0 +1,38 @@
1
+ import logging
2
+ import time
3
+ from dataclasses import dataclass
4
+
5
+ from minisweagent.models import GLOBAL_MODEL_STATS
6
+
7
+
8
+ @dataclass
9
+ class DeterministicModelConfig:
10
+ outputs: list[str]
11
+ model_name: str = "deterministic"
12
+ cost_per_call: float = 1.0
13
+
14
+
15
+ class DeterministicModel:
16
+ def __init__(self, **kwargs):
17
+ """
18
+ Initialize with a list of outputs to return in sequence.
19
+ """
20
+ self.config = DeterministicModelConfig(**kwargs)
21
+ self.current_index = -1
22
+ self.cost = 0.0
23
+ self.n_calls = 0
24
+
25
+ def query(self, messages: list[dict[str, str]], **kwargs) -> dict:
26
+ self.current_index += 1
27
+ output = self.config.outputs[self.current_index]
28
+ if "/sleep" in output:
29
+ print("SLEEPING")
30
+ time.sleep(float(output.split("/sleep")[1]))
31
+ return self.query(messages, **kwargs)
32
+ if "/warning" in output:
33
+ logging.warning(output.split("/warning")[1])
34
+ return self.query(messages, **kwargs)
35
+ self.n_calls += 1
36
+ self.cost += self.config.cost_per_call
37
+ GLOBAL_MODEL_STATS.add(self.config.cost_per_call)
38
+ return {"content": output}
@@ -0,0 +1,42 @@
1
+ def _get_content_text(entry: dict) -> str:
2
+ if isinstance(entry["content"], str):
3
+ return entry["content"]
4
+ assert len(entry["content"]) == 1, "Expected single message in content"
5
+ return entry["content"][0]["text"]
6
+
7
+
8
+ def _clear_cache_control(entry: dict) -> None:
9
+ if isinstance(entry["content"], list):
10
+ assert len(entry["content"]) == 1, "Expected single message in content"
11
+ entry["content"][0].pop("cache_control", None)
12
+ entry.pop("cache_control", None)
13
+
14
+
15
+ def _set_cache_control(entry: dict) -> None:
16
+ if not isinstance(entry["content"], list):
17
+ entry["content"] = [ # type: ignore
18
+ {
19
+ "type": "text",
20
+ "text": _get_content_text(entry),
21
+ "cache_control": {"type": "ephemeral"},
22
+ }
23
+ ]
24
+ else:
25
+ entry["content"][0]["cache_control"] = {"type": "ephemeral"}
26
+ if entry["role"] == "tool":
27
+ # Workaround for weird bug
28
+ entry["content"][0].pop("cache_control", None)
29
+ entry["cache_control"] = {"type": "ephemeral"}
30
+
31
+
32
+ def set_cache_control(messages: list[dict], last_n_messages_offset: int = 0) -> list[dict]:
33
+ """This messages processor adds manual cache control marks to the messages."""
34
+ new_messages = []
35
+ n_tagged = 0
36
+ for i_entry, entry in enumerate(reversed(messages)):
37
+ _clear_cache_control(entry)
38
+ if n_tagged < 2 and entry["role"] in ["user"] and i_entry >= last_n_messages_offset:
39
+ _set_cache_control(entry)
40
+ n_tagged += 1
41
+ new_messages.append(entry)
42
+ return list(reversed(new_messages))
@@ -0,0 +1,18 @@
1
+ """Utility for anthropic where we need different keys for different parallel
2
+ agents to not mess up prompt caching.
3
+ """
4
+
5
+ import threading
6
+ from typing import Any
7
+
8
+ _THREADS_THAT_USED_API_KEYS: list[Any] = []
9
+
10
+
11
+ def get_key_per_thread(api_keys: list[Any]) -> Any:
12
+ """Choose key based on thread name. Returns None if no keys are available."""
13
+ thread_name = threading.current_thread().name
14
+ if thread_name not in _THREADS_THAT_USED_API_KEYS:
15
+ _THREADS_THAT_USED_API_KEYS.append(thread_name)
16
+ thread_idx = _THREADS_THAT_USED_API_KEYS.index(thread_name)
17
+ key_idx = thread_idx % len(api_keys)
18
+ return api_keys[key_idx] or None
minisweagent/py.typed ADDED
File without changes
@@ -0,0 +1 @@
1
+ """Run scripts for mini-SWE-agent."""
File without changes
@@ -0,0 +1,100 @@
1
+ """Utility to manage the global config file.
2
+
3
+ You can also directly edit the `.env` file in the config directory.
4
+
5
+ It is located at [bold green]{global_config_file}[/bold green].
6
+ """
7
+
8
+ import os
9
+ import subprocess
10
+
11
+ from dotenv import set_key, unset_key
12
+ from prompt_toolkit import prompt
13
+ from rich.console import Console
14
+ from rich.rule import Rule
15
+ from typer import Option, Typer
16
+
17
+ from minisweagent import global_config_file
18
+
19
+ app = Typer(
20
+ help=__doc__.format(global_config_file=global_config_file), # type: ignore
21
+ no_args_is_help=True,
22
+ rich_markup_mode="rich",
23
+ add_completion=False,
24
+ )
25
+ console = Console(highlight=False)
26
+
27
+
28
+ _SETUP_HELP = """Welcome to Mini!
29
+
30
+ To get started, we need to set up your global config file.
31
+
32
+ You can edit it manually or use the [bold green]mini-extra config set[/bold green] or [bold green]mini-extra config edit[/bold green] commands.
33
+
34
+ This setup will ask you for your model and an API key.
35
+
36
+ Here's a few popular models and the required API keys:
37
+
38
+ [bold green]claude-sonnet-4-20250514[/bold green] ([bold green]ANTHROPIC_API_KEY[/bold green])
39
+ [bold green]o3[/bold green] ([bold green]OPENAI_API_KEY[/bold green])
40
+
41
+ [bold yellow]You can leave any setting blank to skip it.[/bold yellow]
42
+ """
43
+
44
+
45
+ def configure_if_first_time():
46
+ if not os.getenv("MSWEA_CONFIGURED"):
47
+ console.print(Rule())
48
+ setup()
49
+ console.print(Rule())
50
+
51
+
52
+ @app.command()
53
+ def setup():
54
+ """Setup the global config file."""
55
+ console.print(_SETUP_HELP.format(global_config_file=global_config_file))
56
+ default_model = prompt(
57
+ "Enter your default model (e.g., claude-sonnet-4-20250514): ", default=os.getenv("MSWEA_MODEL_NAME", "")
58
+ ).strip()
59
+ if default_model:
60
+ set_key(global_config_file, "MSWEA_MODEL_NAME", default_model)
61
+ key_name = prompt("Enter your API key name (e.g., ANTHROPIC_API_KEY): ").strip()
62
+ key_value = None
63
+ if key_name:
64
+ key_value = prompt("Enter your API key value (e.g., sk-1234567890): ", default=os.getenv(key_name, "")).strip()
65
+ if key_value:
66
+ set_key(global_config_file, key_name, key_value)
67
+ if not key_value:
68
+ console.print(
69
+ "[bold red]API key setup not completed.[/bold red] Totally fine if you have your keys as environment variables."
70
+ )
71
+ set_key(global_config_file, "MSWEA_CONFIGURED", "true")
72
+ console.print(
73
+ "\n[bold yellow]Config finished.[/bold yellow] If you want to revisit it, run [bold green]mini-extra config setup[/bold green]."
74
+ )
75
+
76
+
77
+ @app.command()
78
+ def set(
79
+ key: str = Option(..., help="The key to set", prompt=True),
80
+ value: str = Option(..., help="The value to set", prompt=True),
81
+ ):
82
+ """Set a key in the global config file."""
83
+ set_key(global_config_file, key, value)
84
+
85
+
86
+ @app.command()
87
+ def unset(key: str = Option(..., help="The key to unset")):
88
+ """Unset a key in the global config file."""
89
+ unset_key(global_config_file, key)
90
+
91
+
92
+ @app.command()
93
+ def edit():
94
+ """Edit the global config file."""
95
+ editor = os.getenv("EDITOR", "nano")
96
+ subprocess.run([editor, global_config_file])
97
+
98
+
99
+ if __name__ == "__main__":
100
+ app()