mita-code 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.
Files changed (67) hide show
  1. mita/__init__.py +5 -0
  2. mita/__main__.py +5 -0
  3. mita/agent/__init__.py +1 -0
  4. mita/agent/context.py +43 -0
  5. mita/agent/conversation.py +101 -0
  6. mita/agent/loop.py +594 -0
  7. mita/agent/system_prompt.py +75 -0
  8. mita/cli.py +940 -0
  9. mita/config/__init__.py +6 -0
  10. mita/config/defaults.py +41 -0
  11. mita/config/loader.py +53 -0
  12. mita/config/schema.py +131 -0
  13. mita/hooks/__init__.py +1 -0
  14. mita/hooks/manager.py +94 -0
  15. mita/hooks/runner.py +145 -0
  16. mita/index/__init__.py +1 -0
  17. mita/index/embeddings.py +49 -0
  18. mita/index/manager.py +170 -0
  19. mita/index/parser.py +331 -0
  20. mita/index/retriever.py +53 -0
  21. mita/index/store.py +143 -0
  22. mita/llm/__init__.py +1 -0
  23. mita/llm/client.py +86 -0
  24. mita/llm/instructor.py +80 -0
  25. mita/llm/streaming.py +58 -0
  26. mita/memory/__init__.py +6 -0
  27. mita/memory/discovery.py +61 -0
  28. mita/memory/loader.py +76 -0
  29. mita/memory/manager.py +117 -0
  30. mita/models/__init__.py +13 -0
  31. mita/models/hardware.py +289 -0
  32. mita/models/manager.py +268 -0
  33. mita/models/ollama_client.py +104 -0
  34. mita/models/recommender.py +88 -0
  35. mita/models/registry.py +167 -0
  36. mita/models/server.py +262 -0
  37. mita/plugins/__init__.py +1 -0
  38. mita/plugins/client.py +152 -0
  39. mita/plugins/manager.py +210 -0
  40. mita/py.typed +0 -0
  41. mita/skills/__init__.py +1 -0
  42. mita/skills/executor.py +84 -0
  43. mita/skills/loader.py +117 -0
  44. mita/skills/manager.py +129 -0
  45. mita/tools/__init__.py +1 -0
  46. mita/tools/builtins/__init__.py +28 -0
  47. mita/tools/builtins/file_edit.py +71 -0
  48. mita/tools/builtins/file_read.py +74 -0
  49. mita/tools/builtins/file_write.py +42 -0
  50. mita/tools/builtins/git.py +112 -0
  51. mita/tools/builtins/glob_tool.py +67 -0
  52. mita/tools/builtins/grep_tool.py +93 -0
  53. mita/tools/builtins/shell.py +83 -0
  54. mita/tools/executor.py +80 -0
  55. mita/tools/registry.py +69 -0
  56. mita/tools/safety.py +91 -0
  57. mita/tools/schema.py +87 -0
  58. mita/ui/__init__.py +1 -0
  59. mita/ui/display.py +139 -0
  60. mita/ui/repl.py +88 -0
  61. mita/ui/spinner.py +48 -0
  62. mita/ui/theme.py +23 -0
  63. mita_code-0.1.0.dist-info/METADATA +227 -0
  64. mita_code-0.1.0.dist-info/RECORD +67 -0
  65. mita_code-0.1.0.dist-info/WHEEL +4 -0
  66. mita_code-0.1.0.dist-info/entry_points.txt +3 -0
  67. mita_code-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,6 @@
1
+ """Configuration loading and schema for Mita Code."""
2
+
3
+ from mita.config.loader import load_config
4
+ from mita.config.schema import MitaConfig
5
+
6
+ __all__ = ["MitaConfig", "load_config"]
@@ -0,0 +1,41 @@
1
+ """Default configuration paths and values."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ # Project-level config directory and file
8
+ PROJECT_CONFIG_DIR = ".mita"
9
+ PROJECT_CONFIG_FILE = "settings.toml"
10
+
11
+
12
+ def get_global_config_dir() -> Path:
13
+ """Return the global config directory."""
14
+ return Path.home() / ".config" / "mita"
15
+
16
+
17
+ def get_global_config_path() -> Path:
18
+ """Return the path to the global config file."""
19
+ return get_global_config_dir() / "config.toml"
20
+
21
+
22
+ def get_project_config_path(project_root: Path | None = None) -> Path | None:
23
+ """Return the path to the project config file, or None if not found."""
24
+ if project_root is None:
25
+ project_root = _find_project_root(Path.cwd())
26
+ if project_root is None:
27
+ return None
28
+ path = project_root / PROJECT_CONFIG_DIR / PROJECT_CONFIG_FILE
29
+ return path if path.is_file() else None
30
+
31
+
32
+ def _find_project_root(start: Path) -> Path | None:
33
+ """Walk up from start to find a project root (contains .git or .mita)."""
34
+ current = start.resolve()
35
+ while True:
36
+ if (current / ".git").exists() or (current / PROJECT_CONFIG_DIR).exists():
37
+ return current
38
+ parent = current.parent
39
+ if parent == current:
40
+ return None
41
+ current = parent
mita/config/loader.py ADDED
@@ -0,0 +1,53 @@
1
+ """TOML config loading with global → project layered merge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tomllib
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from mita.config.defaults import get_global_config_path, get_project_config_path
10
+ from mita.config.schema import MitaConfig
11
+
12
+
13
+ def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
14
+ """Deep merge override into base. Lists are appended, dicts are merged recursively."""
15
+ result = base.copy()
16
+ for key, value in override.items():
17
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
18
+ result[key] = _deep_merge(result[key], value)
19
+ elif key in result and isinstance(result[key], list) and isinstance(value, list):
20
+ result[key] = result[key] + value
21
+ else:
22
+ result[key] = value
23
+ return result
24
+
25
+
26
+ def _load_toml(path: Path) -> dict[str, Any]:
27
+ """Load a TOML file and return its contents as a dict."""
28
+ with open(path, "rb") as f:
29
+ return tomllib.load(f)
30
+
31
+
32
+ def load_config(project_root: Path | None = None) -> MitaConfig:
33
+ """Load and merge configuration from global and project TOML files.
34
+
35
+ Merge strategy:
36
+ - Scalar values: project overrides global
37
+ - Dicts: deep merged recursively
38
+ - Lists (hooks, plugins, skills_paths): project values appended to global
39
+ """
40
+ merged: dict[str, Any] = {}
41
+
42
+ # Load global config
43
+ global_path = get_global_config_path()
44
+ if global_path.is_file():
45
+ merged = _load_toml(global_path)
46
+
47
+ # Load and merge project config
48
+ project_path = get_project_config_path(project_root)
49
+ if project_path is not None:
50
+ project_data = _load_toml(project_path)
51
+ merged = _deep_merge(merged, project_data)
52
+
53
+ return MitaConfig.model_validate(merged)
mita/config/schema.py ADDED
@@ -0,0 +1,131 @@
1
+ """Pydantic models for Mita configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class OllamaSettings(BaseModel):
9
+ """Ollama server connection settings."""
10
+
11
+ host: str = "http://localhost:11434"
12
+ timeout: int = 120
13
+ auto_manage: bool = True
14
+
15
+
16
+ class OllamaRuntimeOptions(BaseModel):
17
+ """Ollama model runtime options passed via the API.
18
+
19
+ These map to Ollama's /api/chat 'options' field and let users
20
+ tune inference for their hardware.
21
+ """
22
+
23
+ num_gpu: int | None = None
24
+ num_thread: int | None = None
25
+ num_ctx: int | None = None
26
+ num_batch: int | None = None
27
+ use_mmap: bool | None = None
28
+ use_mlock: bool | None = None
29
+ num_keep: int | None = None
30
+ main_gpu: int | None = None
31
+ low_vram: bool | None = None
32
+ flash_attention: bool | None = None
33
+
34
+ def to_api_dict(self) -> dict[str, int | bool]:
35
+ """Return only non-None options for the Ollama API."""
36
+ return {k: v for k, v in self.model_dump().items() if v is not None}
37
+
38
+
39
+ class ModelSettings(BaseModel):
40
+ """LLM model settings."""
41
+
42
+ default: str = "qwen2.5-coder:7b"
43
+ embedding: str = "nomic-embed-text"
44
+ temperature: float = 0.1
45
+ max_tokens: int = 4096
46
+ context_window: int = 32768
47
+ ollama_options: OllamaRuntimeOptions = Field(default_factory=OllamaRuntimeOptions)
48
+
49
+
50
+ class ToolSettings(BaseModel):
51
+ """Tool execution settings."""
52
+
53
+ auto_approve: list[str] = Field(default_factory=lambda: ["file_read", "glob", "grep"])
54
+ confirm_destructive: bool = True
55
+ shell_timeout: int = 120
56
+ banned_commands: list[str] = Field(
57
+ default_factory=lambda: ["rm -rf /", "mkfs", "dd if=/dev/zero"]
58
+ )
59
+
60
+
61
+ class MemorySettings(BaseModel):
62
+ """MITA.md memory system settings."""
63
+
64
+ max_lines_per_file: int = 200
65
+ max_total_tokens: int = 4000
66
+
67
+
68
+ class IndexSettings(BaseModel):
69
+ """Codebase indexing settings."""
70
+
71
+ enabled: bool = True
72
+ chunk_size: int = 512
73
+ chunk_overlap: int = 64
74
+ top_k: int = 10
75
+ exclude_patterns: list[str] = Field(
76
+ default_factory=lambda: [
77
+ "*.lock",
78
+ ".mita/**",
79
+ "node_modules/**",
80
+ ".git/**",
81
+ "*.min.js",
82
+ "*.min.css",
83
+ "dist/**",
84
+ "build/**",
85
+ "__pycache__/**",
86
+ ]
87
+ )
88
+
89
+
90
+ class HookDefinition(BaseModel):
91
+ """A lifecycle hook definition."""
92
+
93
+ event: str
94
+ command: str
95
+ match: str | None = None
96
+
97
+
98
+ class PluginDefinition(BaseModel):
99
+ """An MCP plugin server definition."""
100
+
101
+ name: str
102
+ transport: str = "stdio"
103
+ command: str | None = None
104
+ args: list[str] = Field(default_factory=list)
105
+ url: str | None = None
106
+ env: dict[str, str] = Field(default_factory=dict)
107
+
108
+
109
+ class UISettings(BaseModel):
110
+ """Terminal UI settings."""
111
+
112
+ theme: str = "auto"
113
+ show_token_count: bool = True
114
+ stream: bool = True
115
+ markdown: bool = True
116
+
117
+
118
+ class MitaConfig(BaseModel):
119
+ """Root configuration model — result of merging global + project TOML."""
120
+
121
+ ollama: OllamaSettings = Field(default_factory=OllamaSettings)
122
+ model: ModelSettings = Field(default_factory=ModelSettings)
123
+ tools: ToolSettings = Field(default_factory=ToolSettings)
124
+ memory: MemorySettings = Field(default_factory=MemorySettings)
125
+ index: IndexSettings = Field(default_factory=IndexSettings)
126
+ ui: UISettings = Field(default_factory=UISettings)
127
+ hooks: list[HookDefinition] = Field(default_factory=list)
128
+ plugins: list[PluginDefinition] = Field(default_factory=list)
129
+ skills_paths: list[str] = Field(
130
+ default_factory=lambda: ["~/.config/mita/skills", ".mita/skills"]
131
+ )
mita/hooks/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Lifecycle hooks — user-configured shell commands at trigger points."""
mita/hooks/manager.py ADDED
@@ -0,0 +1,94 @@
1
+ """Hook management — CLI helpers for listing and adding hooks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+
7
+ from mita.config.loader import load_config
8
+ from mita.hooks.runner import VALID_EVENTS
9
+
10
+ console = Console()
11
+
12
+
13
+ def list_hooks() -> None:
14
+ """List all configured hooks."""
15
+ cfg = load_config()
16
+ if not cfg.hooks:
17
+ console.print("[dim]No hooks configured.[/dim]")
18
+ return
19
+
20
+ for hook in cfg.hooks:
21
+ match_str = f" (match: {hook.match})" if hook.match else ""
22
+ console.print(f" [{hook.event}] {hook.command}{match_str}")
23
+
24
+
25
+ def add_hook(event: str, command: str, match: str | None = None) -> None:
26
+ """Add a hook to the project config."""
27
+ if event not in VALID_EVENTS:
28
+ console.print(f"[red]Invalid event '{event}'.[/red]")
29
+ console.print(f"Valid events: {', '.join(sorted(VALID_EVENTS))}")
30
+ return
31
+
32
+ from pathlib import Path
33
+
34
+ from mita.config.defaults import (
35
+ PROJECT_CONFIG_DIR,
36
+ PROJECT_CONFIG_FILE,
37
+ _find_project_root,
38
+ get_project_config_path,
39
+ )
40
+
41
+ project_path = get_project_config_path()
42
+ if not project_path:
43
+ root = _find_project_root(Path.cwd())
44
+ if root is None:
45
+ console.print("[red]Not in a project directory (no .git or .mita/).[/red]")
46
+ return
47
+ config_dir = root / PROJECT_CONFIG_DIR
48
+ config_dir.mkdir(exist_ok=True)
49
+ project_path = config_dir / PROJECT_CONFIG_FILE
50
+ project_path.touch()
51
+
52
+ lines = [f'\n[[hooks]]\nevent = "{event}"\ncommand = "{command}"']
53
+ if match:
54
+ lines.append(f'match = "{match}"')
55
+
56
+ block = "\n".join(lines) + "\n"
57
+
58
+ with open(project_path, "a") as f:
59
+ f.write(block)
60
+
61
+ console.print(f"[green]Hook added: [{event}] {command}[/green]")
62
+
63
+
64
+ def remove_hooks(event: str) -> None:
65
+ """Remove all hooks for an event from the project config."""
66
+ import tomllib
67
+
68
+ from mita.config.defaults import get_project_config_path
69
+
70
+ project_path = get_project_config_path()
71
+ if not project_path or not project_path.is_file():
72
+ console.print("[red]No project config found.[/red]")
73
+ return
74
+
75
+ with open(project_path, "rb") as f:
76
+ data = tomllib.load(f)
77
+
78
+ hooks = data.get("hooks", [])
79
+ new_hooks = [h for h in hooks if h.get("event") != event]
80
+ if len(new_hooks) == len(hooks):
81
+ console.print(f"[yellow]No hooks found for event '{event}'.[/yellow]")
82
+ return
83
+
84
+ removed = len(hooks) - len(new_hooks)
85
+ data["hooks"] = new_hooks
86
+
87
+ # Rewrite the TOML file
88
+ from mita.cli import _dict_to_toml
89
+
90
+ lines: list[str] = []
91
+ _dict_to_toml(data, lines, prefix="")
92
+
93
+ project_path.write_text("\n".join(lines) + "\n")
94
+ console.print(f"[green]Removed {removed} hook(s) for event '{event}'.[/green]")
mita/hooks/runner.py ADDED
@@ -0,0 +1,145 @@
1
+ """Execute lifecycle hooks with context variable injection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import fnmatch
7
+ import subprocess
8
+ from typing import Any
9
+
10
+ from rich.console import Console
11
+
12
+ from mita.config.schema import HookDefinition
13
+
14
+ # Valid hook events
15
+ VALID_EVENTS = frozenset(
16
+ {
17
+ "session_start",
18
+ "session_end",
19
+ "pre_tool_call",
20
+ "post_tool_call",
21
+ "on_file_write",
22
+ }
23
+ )
24
+
25
+ # Default timeout for hook commands (seconds)
26
+ HOOK_TIMEOUT = 30
27
+
28
+
29
+ def _matches(hook: HookDefinition, context: dict[str, Any] | None) -> bool:
30
+ """Check if a hook's match pattern applies to the context."""
31
+ if hook.match is None:
32
+ return True
33
+ if context is None:
34
+ return True
35
+
36
+ # Match against file_path for on_file_write
37
+ file_path = context.get("file_path")
38
+ if file_path and fnmatch.fnmatch(str(file_path), hook.match):
39
+ return True
40
+
41
+ # Match against tool name for pre/post_tool_call
42
+ tool = context.get("tool")
43
+ if tool and fnmatch.fnmatch(str(tool), hook.match):
44
+ return True
45
+
46
+ return False
47
+
48
+
49
+ def _render_command(command: str, context: dict[str, Any] | None) -> str:
50
+ """Substitute context variables into a hook command string."""
51
+ if context is None:
52
+ return command
53
+
54
+ rendered = command
55
+ for key, value in context.items():
56
+ rendered = rendered.replace(f"{{{key}}}", str(value))
57
+ return rendered
58
+
59
+
60
+ async def run_hooks(
61
+ event: str,
62
+ hooks: list[HookDefinition],
63
+ context: dict[str, Any] | None = None,
64
+ console: Console | None = None,
65
+ timeout: float = HOOK_TIMEOUT,
66
+ ) -> list[dict[str, Any]]:
67
+ """Execute all hooks matching the given event.
68
+
69
+ Args:
70
+ event: Hook event name (e.g., "session_start", "on_file_write").
71
+ hooks: List of hook definitions from config.
72
+ context: Optional context dict with vars like {file_path}, {tool}.
73
+ console: Optional console for displaying hook output.
74
+ timeout: Timeout in seconds for each hook command.
75
+
76
+ Returns:
77
+ List of result dicts with keys: event, command, returncode, stdout, stderr.
78
+ """
79
+ results: list[dict[str, Any]] = []
80
+
81
+ matching = [h for h in hooks if h.event == event and _matches(h, context)]
82
+ if not matching:
83
+ return results
84
+
85
+ for hook in matching:
86
+ command = _render_command(hook.command, context)
87
+ result = await _execute_hook(command, timeout=timeout)
88
+ result["event"] = event
89
+ results.append(result)
90
+
91
+ if console:
92
+ if result["returncode"] != 0:
93
+ console.print(
94
+ f" [yellow]Hook ({event}): '{command}' "
95
+ f"exited with code {result['returncode']}[/yellow]"
96
+ )
97
+ if result["stderr"]:
98
+ console.print(f" [dim]{result['stderr'][:200]}[/dim]")
99
+ elif result["stdout"]:
100
+ # Show truncated output for successful hooks
101
+ stdout = result["stdout"].strip()
102
+ if stdout:
103
+ lines = stdout.split("\n")
104
+ preview = lines[0][:100]
105
+ if len(lines) > 1:
106
+ preview += f" (+{len(lines) - 1} more lines)"
107
+ console.print(f" [dim]Hook ({event}): {preview}[/dim]")
108
+
109
+ return results
110
+
111
+
112
+ async def _execute_hook(command: str, timeout: float = HOOK_TIMEOUT) -> dict[str, Any]:
113
+ """Execute a single hook command asynchronously."""
114
+ try:
115
+ result = await asyncio.wait_for(
116
+ asyncio.to_thread(
117
+ subprocess.run,
118
+ command,
119
+ shell=True,
120
+ capture_output=True,
121
+ text=True,
122
+ timeout=timeout,
123
+ ),
124
+ timeout=timeout + 5, # extra buffer for asyncio
125
+ )
126
+ return {
127
+ "command": command,
128
+ "returncode": result.returncode,
129
+ "stdout": result.stdout,
130
+ "stderr": result.stderr,
131
+ }
132
+ except (TimeoutError, subprocess.TimeoutExpired):
133
+ return {
134
+ "command": command,
135
+ "returncode": -1,
136
+ "stdout": "",
137
+ "stderr": f"Hook timed out after {timeout}s",
138
+ }
139
+ except OSError as e:
140
+ return {
141
+ "command": command,
142
+ "returncode": -1,
143
+ "stdout": "",
144
+ "stderr": f"Hook execution failed: {e}",
145
+ }
mita/index/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Codebase indexing: Tree-sitter parsing, embeddings, LanceDB vector store."""
@@ -0,0 +1,49 @@
1
+ """Embedding generation via Ollama for code chunks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ollama
6
+
7
+ from mita.config.schema import MitaConfig
8
+
9
+ BATCH_SIZE = 32
10
+
11
+
12
+ class EmbeddingClient:
13
+ """Generate embeddings using Ollama's embedding endpoint."""
14
+
15
+ def __init__(self, config: MitaConfig) -> None:
16
+ self._model = config.model.embedding
17
+ self._client = ollama.AsyncClient(host=config.ollama.host)
18
+
19
+ @property
20
+ def model(self) -> str:
21
+ return self._model
22
+
23
+ async def embed_texts(self, texts: list[str]) -> list[list[float]]:
24
+ """Generate embeddings for a batch of texts."""
25
+ all_embeddings: list[list[float]] = []
26
+ for i in range(0, len(texts), BATCH_SIZE):
27
+ batch = texts[i : i + BATCH_SIZE]
28
+ response = await self._client.embed(model=self._model, input=batch)
29
+ all_embeddings.extend(list(e) for e in response.embeddings)
30
+ return all_embeddings
31
+
32
+ async def embed_single(self, text: str) -> list[float]:
33
+ """Generate embedding for a single text."""
34
+ response = await self._client.embed(model=self._model, input=[text])
35
+ return list(response.embeddings[0])
36
+
37
+ async def is_model_available(self) -> bool:
38
+ """Check if the embedding model is pulled in Ollama."""
39
+ try:
40
+ models = await self._client.list()
41
+ model_names = [m.model for m in models.models]
42
+ # Check both exact match and base name (without tag)
43
+ base_name = self._model.split(":")[0]
44
+ return any(
45
+ m == self._model or (m is not None and m.startswith(base_name + ":"))
46
+ for m in model_names
47
+ )
48
+ except Exception: # noqa: BLE001
49
+ return False