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.
- mita/__init__.py +5 -0
- mita/__main__.py +5 -0
- mita/agent/__init__.py +1 -0
- mita/agent/context.py +43 -0
- mita/agent/conversation.py +101 -0
- mita/agent/loop.py +594 -0
- mita/agent/system_prompt.py +75 -0
- mita/cli.py +940 -0
- mita/config/__init__.py +6 -0
- mita/config/defaults.py +41 -0
- mita/config/loader.py +53 -0
- mita/config/schema.py +131 -0
- mita/hooks/__init__.py +1 -0
- mita/hooks/manager.py +94 -0
- mita/hooks/runner.py +145 -0
- mita/index/__init__.py +1 -0
- mita/index/embeddings.py +49 -0
- mita/index/manager.py +170 -0
- mita/index/parser.py +331 -0
- mita/index/retriever.py +53 -0
- mita/index/store.py +143 -0
- mita/llm/__init__.py +1 -0
- mita/llm/client.py +86 -0
- mita/llm/instructor.py +80 -0
- mita/llm/streaming.py +58 -0
- mita/memory/__init__.py +6 -0
- mita/memory/discovery.py +61 -0
- mita/memory/loader.py +76 -0
- mita/memory/manager.py +117 -0
- mita/models/__init__.py +13 -0
- mita/models/hardware.py +289 -0
- mita/models/manager.py +268 -0
- mita/models/ollama_client.py +104 -0
- mita/models/recommender.py +88 -0
- mita/models/registry.py +167 -0
- mita/models/server.py +262 -0
- mita/plugins/__init__.py +1 -0
- mita/plugins/client.py +152 -0
- mita/plugins/manager.py +210 -0
- mita/py.typed +0 -0
- mita/skills/__init__.py +1 -0
- mita/skills/executor.py +84 -0
- mita/skills/loader.py +117 -0
- mita/skills/manager.py +129 -0
- mita/tools/__init__.py +1 -0
- mita/tools/builtins/__init__.py +28 -0
- mita/tools/builtins/file_edit.py +71 -0
- mita/tools/builtins/file_read.py +74 -0
- mita/tools/builtins/file_write.py +42 -0
- mita/tools/builtins/git.py +112 -0
- mita/tools/builtins/glob_tool.py +67 -0
- mita/tools/builtins/grep_tool.py +93 -0
- mita/tools/builtins/shell.py +83 -0
- mita/tools/executor.py +80 -0
- mita/tools/registry.py +69 -0
- mita/tools/safety.py +91 -0
- mita/tools/schema.py +87 -0
- mita/ui/__init__.py +1 -0
- mita/ui/display.py +139 -0
- mita/ui/repl.py +88 -0
- mita/ui/spinner.py +48 -0
- mita/ui/theme.py +23 -0
- mita_code-0.1.0.dist-info/METADATA +227 -0
- mita_code-0.1.0.dist-info/RECORD +67 -0
- mita_code-0.1.0.dist-info/WHEEL +4 -0
- mita_code-0.1.0.dist-info/entry_points.txt +3 -0
- mita_code-0.1.0.dist-info/licenses/LICENSE +201 -0
mita/config/__init__.py
ADDED
mita/config/defaults.py
ADDED
|
@@ -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."""
|
mita/index/embeddings.py
ADDED
|
@@ -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
|