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
mita/tools/safety.py ADDED
@@ -0,0 +1,91 @@
1
+ """Destructive action detection and banned command checking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from mita.config.schema import ToolSettings
8
+ from mita.tools.schema import ToolCall, ToolDefinition
9
+
10
+ # Patterns that indicate destructive shell commands
11
+ DESTRUCTIVE_PATTERNS: list[re.Pattern[str]] = [
12
+ re.compile(r"\brm\s+(-[a-zA-Z]*f|-[a-zA-Z]*r)", re.IGNORECASE),
13
+ re.compile(r"\bgit\s+(push\s+--force|reset\s+--hard|clean\s+-[a-zA-Z]*f)", re.IGNORECASE),
14
+ re.compile(r"\bchmod\s+-R\s+0?7", re.IGNORECASE),
15
+ re.compile(r"\bchown\s+-R\b", re.IGNORECASE),
16
+ re.compile(r"\b(truncate|shred)\b", re.IGNORECASE),
17
+ re.compile(r">\s*/dev/\w+", re.IGNORECASE),
18
+ ]
19
+
20
+ # Git subcommands that are destructive and need confirmation
21
+ DESTRUCTIVE_GIT_PATTERNS: list[re.Pattern[str]] = [
22
+ re.compile(r"^push\s+--force", re.IGNORECASE),
23
+ re.compile(r"^push\s+-f\b", re.IGNORECASE),
24
+ re.compile(r"^reset\s+--hard", re.IGNORECASE),
25
+ re.compile(r"^clean\s+-[a-zA-Z]*f", re.IGNORECASE),
26
+ re.compile(r"^checkout\s+--\s", re.IGNORECASE),
27
+ re.compile(r"^branch\s+-[dD]\b", re.IGNORECASE),
28
+ ]
29
+
30
+
31
+ def is_command_banned(command: str, banned_commands: list[str]) -> bool:
32
+ """Check if a shell command matches any banned command pattern."""
33
+ normalized = command.strip()
34
+ for banned in banned_commands:
35
+ if banned in normalized:
36
+ return True
37
+ return False
38
+
39
+
40
+ def is_command_destructive(command: str) -> bool:
41
+ """Check if a shell command looks destructive."""
42
+ for pattern in DESTRUCTIVE_PATTERNS:
43
+ if pattern.search(command):
44
+ return True
45
+ return False
46
+
47
+
48
+ def is_git_command_destructive(subcommand: str) -> bool:
49
+ """Check if a git subcommand is destructive."""
50
+ stripped = subcommand.strip()
51
+ for pattern in DESTRUCTIVE_GIT_PATTERNS:
52
+ if pattern.search(stripped):
53
+ return True
54
+ return False
55
+
56
+
57
+ def needs_confirmation(
58
+ tool_call: ToolCall,
59
+ tool_def: ToolDefinition,
60
+ settings: ToolSettings,
61
+ ) -> bool:
62
+ """Determine if a tool call needs user confirmation before execution.
63
+
64
+ Returns True if:
65
+ - The tool is marked destructive AND confirm_destructive is on
66
+ AND the tool is not in auto_approve.
67
+ - Or the tool is 'shell' and the command looks destructive.
68
+ - Or the tool is 'git' and the subcommand is destructive.
69
+ """
70
+ if not settings.confirm_destructive:
71
+ return False
72
+
73
+ if tool_call.name in settings.auto_approve:
74
+ return False
75
+
76
+ if tool_def.destructive:
77
+ return True
78
+
79
+ # Extra check for shell commands that look destructive
80
+ if tool_call.name == "shell":
81
+ command = tool_call.arguments.get("command", "")
82
+ if is_command_destructive(command):
83
+ return True
84
+
85
+ # Check git subcommands for destructive operations
86
+ if tool_call.name == "git":
87
+ subcommand = tool_call.arguments.get("subcommand", "")
88
+ if is_git_command_destructive(subcommand):
89
+ return True
90
+
91
+ return False
mita/tools/schema.py ADDED
@@ -0,0 +1,87 @@
1
+ """Pydantic models for tool definitions, calls, and results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class ToolParameter(BaseModel):
11
+ """A parameter in a tool definition."""
12
+
13
+ name: str
14
+ type: str # "string", "integer", "boolean", "array", "object"
15
+ description: str
16
+ required: bool = True
17
+ default: Any | None = None
18
+
19
+
20
+ class ToolDefinition(BaseModel):
21
+ """Schema sent to the LLM so it knows what tools are available."""
22
+
23
+ name: str
24
+ description: str
25
+ parameters: list[ToolParameter]
26
+ destructive: bool = False # triggers confirmation flow
27
+ source: str = "builtin" # "builtin" | "mcp:<plugin_name>"
28
+
29
+ def to_openai_schema(self) -> dict[str, Any]:
30
+ """Convert to OpenAI-compatible function schema for LiteLLM."""
31
+ properties: dict[str, Any] = {}
32
+ required: list[str] = []
33
+
34
+ for param in self.parameters:
35
+ prop: dict[str, Any] = {
36
+ "type": param.type,
37
+ "description": param.description,
38
+ }
39
+ if param.default is not None:
40
+ prop["default"] = param.default
41
+ properties[param.name] = prop
42
+ if param.required:
43
+ required.append(param.name)
44
+
45
+ return {
46
+ "type": "function",
47
+ "function": {
48
+ "name": self.name,
49
+ "description": self.description,
50
+ "parameters": {
51
+ "type": "object",
52
+ "properties": properties,
53
+ "required": required,
54
+ },
55
+ },
56
+ }
57
+
58
+
59
+ class ToolCall(BaseModel):
60
+ """Parsed from LLM output."""
61
+
62
+ id: str
63
+ name: str
64
+ arguments: dict[str, Any]
65
+
66
+
67
+ class ToolResult(BaseModel):
68
+ """Returned after executing a tool."""
69
+
70
+ tool_call_id: str
71
+ success: bool
72
+ output: str = ""
73
+ error: str | None = None
74
+ truncated: bool = False
75
+
76
+ # Max output size before truncation (10K chars)
77
+ MAX_OUTPUT_SIZE: int = Field(default=10_000, exclude=True)
78
+
79
+ def truncate_output(self) -> ToolResult:
80
+ """Return a copy with output truncated if too long."""
81
+ if len(self.output) <= self.MAX_OUTPUT_SIZE:
82
+ return self
83
+ truncated_output = (
84
+ self.output[: self.MAX_OUTPUT_SIZE]
85
+ + f"\n\n... [truncated, {len(self.output):,} chars total]"
86
+ )
87
+ return self.model_copy(update={"output": truncated_output, "truncated": True})
mita/ui/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Terminal UI: REPL, Rich rendering, spinners, themes."""
mita/ui/display.py ADDED
@@ -0,0 +1,139 @@
1
+ """Rich terminal rendering: markdown, code blocks, diffs, tool calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+ from rich.markdown import Markdown
7
+ from rich.panel import Panel
8
+
9
+ from mita.tools.schema import ToolCall, ToolResult
10
+ from mita.ui.theme import MITA_THEME
11
+
12
+ # Module-level console with mita theme
13
+ _console: Console | None = None
14
+
15
+
16
+ def get_console() -> Console:
17
+ """Get or create the themed console."""
18
+ global _console # noqa: PLW0603
19
+ if _console is None:
20
+ _console = Console(theme=MITA_THEME)
21
+ return _console
22
+
23
+
24
+ def display_welcome(console: Console) -> None:
25
+ """Display the welcome banner."""
26
+ console.print(
27
+ Panel(
28
+ "[mita.prompt]mita[/mita.prompt] — local-first coding assistant\n"
29
+ "[mita.dim]Type your prompt, or /quit to exit.[/mita.dim]",
30
+ border_style="mita.info",
31
+ )
32
+ )
33
+
34
+
35
+ def display_goodbye(console: Console) -> None:
36
+ """Display the exit message."""
37
+ console.print("[mita.dim]Goodbye![/mita.dim]")
38
+
39
+
40
+ def display_markdown(console: Console, text: str) -> None:
41
+ """Render markdown text in the terminal."""
42
+ if not text.strip():
43
+ return
44
+ md = Markdown(text)
45
+ console.print(md)
46
+
47
+
48
+ def display_streaming_token(console: Console, token: str) -> None:
49
+ """Display a single streaming token (no newline)."""
50
+ console.print(token, end="", highlight=False)
51
+
52
+
53
+ def display_streaming_end(console: Console) -> None:
54
+ """End a streaming output block."""
55
+ console.print() # final newline
56
+
57
+
58
+ def display_tool_call(console: Console, tool_call: ToolCall) -> None:
59
+ """Display a tool call that is about to be executed."""
60
+ args_str = ", ".join(f"{k}={v!r}" for k, v in tool_call.arguments.items())
61
+ # Truncate long args for display
62
+ if len(args_str) > 200:
63
+ args_str = args_str[:200] + "..."
64
+ console.print(
65
+ f" [mita.tool_name]{tool_call.name}[/mita.tool_name]({args_str})",
66
+ )
67
+
68
+
69
+ def display_tool_result(console: Console, result: ToolResult) -> None:
70
+ """Display the result of a tool execution."""
71
+ if result.success:
72
+ if result.output:
73
+ # Show truncated output
74
+ output = result.output
75
+ if len(output) > 500:
76
+ output = output[:500] + "\n..."
77
+ console.print(f" [mita.dim]{output}[/mita.dim]")
78
+ else:
79
+ console.print(f" [mita.tool_error]Error: {result.error}[/mita.tool_error]")
80
+
81
+
82
+ def display_error(console: Console, message: str) -> None:
83
+ """Display an error message."""
84
+ console.print(f"[mita.error]{message}[/mita.error]")
85
+
86
+
87
+ def display_warning(console: Console, message: str) -> None:
88
+ """Display a warning message."""
89
+ console.print(f"[mita.warning]{message}[/mita.warning]")
90
+
91
+
92
+ def display_token_usage(console: Console, total_tokens: int) -> None:
93
+ """Display token usage after a response (estimated)."""
94
+ console.print(f"[mita.token_count]({total_tokens:,} tokens)[/mita.token_count]")
95
+
96
+
97
+ def display_response_stats(
98
+ console: Console,
99
+ prompt_tokens: int,
100
+ completion_tokens: int,
101
+ total_time: float,
102
+ ttft: float | None = None,
103
+ ) -> None:
104
+ """Display detailed response statistics after an LLM call."""
105
+ parts: list[str] = []
106
+
107
+ parts.append(f"prompt: {prompt_tokens:,}")
108
+ parts.append(f"completion: {completion_tokens:,}")
109
+
110
+ if ttft is not None:
111
+ parts.append(f"ttft: {ttft:.1f}s")
112
+
113
+ parts.append(f"total: {total_time:.1f}s")
114
+
115
+ if completion_tokens > 0 and total_time > 0:
116
+ tps = completion_tokens / total_time
117
+ parts.append(f"{tps:.1f} tok/s")
118
+
119
+ console.print(f"[mita.token_count]({' · '.join(parts)})[/mita.token_count]")
120
+
121
+
122
+ def display_error_with_suggestion(console: Console, error: str, suggestion: str) -> None:
123
+ """Display an error with an actionable suggestion."""
124
+ console.print(f"[red]{error}[/red]")
125
+ console.print(f"[dim] → {suggestion}[/dim]")
126
+
127
+
128
+ async def prompt_user_confirm(console: Console, question: str) -> bool:
129
+ """Ask the user for confirmation before a destructive action.
130
+
131
+ Returns True if the user approves, False otherwise.
132
+ """
133
+ console.print(f"[mita.warning]{question}[/mita.warning] ", end="")
134
+ try:
135
+ response = console.input("[y/N] ")
136
+ return response.strip().lower() in ("y", "yes")
137
+ except (EOFError, KeyboardInterrupt):
138
+ console.print()
139
+ return False
mita/ui/repl.py ADDED
@@ -0,0 +1,88 @@
1
+ """Interactive REPL input loop with prompt_toolkit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Coroutine
6
+ from typing import Any
7
+
8
+ from prompt_toolkit import PromptSession
9
+ from prompt_toolkit.history import InMemoryHistory
10
+ from rich.console import Console
11
+
12
+ from mita.ui.display import display_goodbye, display_welcome
13
+
14
+ # Built-in REPL commands that should NOT be treated as skill invocations.
15
+ _BUILTIN_COMMANDS = frozenset({"/quit", "/exit", "/q", "/clear"})
16
+
17
+
18
+ async def repl_loop(
19
+ console: Console,
20
+ on_input: Callable[[str], Coroutine[Any, Any, None]],
21
+ on_clear: Callable[[], None] | None = None,
22
+ skills_paths: list[str] | None = None,
23
+ ) -> None:
24
+ """Run the interactive REPL.
25
+
26
+ Args:
27
+ console: Rich console for output.
28
+ on_input: Async callback called with each user input line.
29
+ on_clear: Callback invoked when the user types /clear.
30
+ skills_paths: Skill search paths for ``/`` prefix invocation.
31
+ """
32
+ display_welcome(console)
33
+
34
+ session: PromptSession[str] = PromptSession(history=InMemoryHistory())
35
+
36
+ while True:
37
+ try:
38
+ user_input = await session.prompt_async("mita> ")
39
+ except (EOFError, KeyboardInterrupt):
40
+ display_goodbye(console)
41
+ break
42
+
43
+ user_input = user_input.strip()
44
+ if not user_input:
45
+ continue
46
+
47
+ if user_input.lower() in ("/quit", "/exit", "/q"):
48
+ display_goodbye(console)
49
+ break
50
+
51
+ if user_input.lower() == "/clear":
52
+ if on_clear is not None:
53
+ on_clear()
54
+ console.print("[mita.dim]Conversation cleared.[/mita.dim]")
55
+ continue
56
+
57
+ # Skill invocation: /name [args]
58
+ if user_input.startswith("/") and user_input.split()[0].lower() not in _BUILTIN_COMMANDS:
59
+ rendered = _try_render_skill(user_input, skills_paths or [], console)
60
+ if rendered is not None:
61
+ try:
62
+ await on_input(rendered)
63
+ except KeyboardInterrupt:
64
+ console.print("\n[Interrupted]")
65
+ continue
66
+
67
+ try:
68
+ await on_input(user_input)
69
+ except KeyboardInterrupt:
70
+ console.print("\n[Interrupted]")
71
+
72
+
73
+ def _try_render_skill(user_input: str, skills_paths: list[str], console: Console) -> str | None:
74
+ """Attempt to find and render a skill from user input.
75
+
76
+ Returns the rendered prompt string, or ``None`` if no matching skill was found.
77
+ """
78
+ from mita.skills.executor import render_skill
79
+ from mita.skills.loader import discover_skills, find_skill
80
+
81
+ skills = discover_skills(skills_paths)
82
+ skill = find_skill(skills, user_input)
83
+ if skill is None:
84
+ return None
85
+
86
+ rendered = render_skill(skill, user_input)
87
+ console.print(f"[dim]Running skill: {skill.frontmatter.name}[/dim]")
88
+ return rendered
mita/ui/spinner.py ADDED
@@ -0,0 +1,48 @@
1
+ """Thinking/loading indicators for the terminal."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections.abc import Generator
7
+ from contextlib import contextmanager
8
+
9
+ from rich.console import Console, RenderableType
10
+ from rich.live import Live
11
+ from rich.spinner import Spinner as RichSpinner
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+
15
+
16
+ class _TimedSpinner:
17
+ """A spinner that shows elapsed time alongside the message."""
18
+
19
+ def __init__(self, message: str = "Thinking...") -> None:
20
+ self._message = message
21
+ self._start = time.monotonic()
22
+ self._spinner = RichSpinner("dots")
23
+
24
+ def __rich__(self) -> RenderableType:
25
+ elapsed = time.monotonic() - self._start
26
+ table = Table.grid(padding=(0, 1))
27
+ table.add_row(
28
+ self._spinner,
29
+ Text(self._message, style="mita.dim"),
30
+ Text(f"({elapsed:.0f}s)", style="dim"),
31
+ )
32
+ return table
33
+
34
+
35
+ @contextmanager
36
+ def thinking_spinner(
37
+ console: Console,
38
+ message: str = "Thinking...",
39
+ ) -> Generator[Live, None, None]:
40
+ """Display a spinner with elapsed time while the agent is thinking.
41
+
42
+ Usage:
43
+ with thinking_spinner(console):
44
+ await long_operation()
45
+ """
46
+ spinner = _TimedSpinner(message)
47
+ with Live(spinner, console=console, transient=True, refresh_per_second=4) as live:
48
+ yield live
mita/ui/theme.py ADDED
@@ -0,0 +1,23 @@
1
+ """Color scheme and style constants for terminal UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.style import Style
6
+ from rich.theme import Theme
7
+
8
+ # Style constants
9
+ STYLES = {
10
+ "mita.prompt": Style(color="cyan", bold=True),
11
+ "mita.assistant": Style(color="white"),
12
+ "mita.tool_name": Style(color="yellow", bold=True),
13
+ "mita.tool_result": Style(color="green"),
14
+ "mita.tool_error": Style(color="red"),
15
+ "mita.info": Style(color="blue"),
16
+ "mita.warning": Style(color="yellow"),
17
+ "mita.error": Style(color="red", bold=True),
18
+ "mita.dim": Style(dim=True),
19
+ "mita.success": Style(color="green", bold=True),
20
+ "mita.token_count": Style(color="cyan", dim=True),
21
+ }
22
+
23
+ MITA_THEME = Theme(STYLES)