minion-code 0.1.0__py3-none-any.whl → 0.1.1__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.
- examples/cli_entrypoint.py +60 -0
- examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
- examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
- examples/components/messages_component.py +199 -0
- examples/file_freshness_example.py +22 -22
- examples/file_watching_example.py +32 -26
- examples/interruptible_tui.py +921 -3
- examples/repl_tui.py +129 -0
- examples/skills/example_usage.py +57 -0
- examples/start.py +173 -0
- minion_code/__init__.py +1 -1
- minion_code/acp_server/__init__.py +34 -0
- minion_code/acp_server/agent.py +539 -0
- minion_code/acp_server/hooks.py +354 -0
- minion_code/acp_server/main.py +194 -0
- minion_code/acp_server/permissions.py +142 -0
- minion_code/acp_server/test_client.py +104 -0
- minion_code/adapters/__init__.py +22 -0
- minion_code/adapters/output_adapter.py +207 -0
- minion_code/adapters/rich_adapter.py +169 -0
- minion_code/adapters/textual_adapter.py +254 -0
- minion_code/agents/__init__.py +2 -2
- minion_code/agents/code_agent.py +517 -104
- minion_code/agents/hooks.py +378 -0
- minion_code/cli.py +538 -429
- minion_code/cli_simple.py +665 -0
- minion_code/commands/__init__.py +136 -29
- minion_code/commands/clear_command.py +19 -46
- minion_code/commands/help_command.py +33 -49
- minion_code/commands/history_command.py +37 -55
- minion_code/commands/model_command.py +194 -0
- minion_code/commands/quit_command.py +9 -12
- minion_code/commands/resume_command.py +181 -0
- minion_code/commands/skill_command.py +89 -0
- minion_code/commands/status_command.py +48 -73
- minion_code/commands/tools_command.py +54 -52
- minion_code/commands/version_command.py +34 -69
- minion_code/components/ConfirmDialog.py +430 -0
- minion_code/components/Message.py +318 -97
- minion_code/components/MessageResponse.py +30 -29
- minion_code/components/Messages.py +351 -0
- minion_code/components/PromptInput.py +499 -245
- minion_code/components/__init__.py +24 -17
- minion_code/const.py +7 -0
- minion_code/screens/REPL.py +1453 -469
- minion_code/screens/__init__.py +1 -1
- minion_code/services/__init__.py +20 -20
- minion_code/services/event_system.py +19 -14
- minion_code/services/file_freshness_service.py +223 -170
- minion_code/skills/__init__.py +25 -0
- minion_code/skills/skill.py +128 -0
- minion_code/skills/skill_loader.py +198 -0
- minion_code/skills/skill_registry.py +177 -0
- minion_code/subagents/__init__.py +31 -0
- minion_code/subagents/builtin/__init__.py +30 -0
- minion_code/subagents/builtin/claude_code_guide.py +32 -0
- minion_code/subagents/builtin/explore.py +36 -0
- minion_code/subagents/builtin/general_purpose.py +19 -0
- minion_code/subagents/builtin/plan.py +61 -0
- minion_code/subagents/subagent.py +116 -0
- minion_code/subagents/subagent_loader.py +147 -0
- minion_code/subagents/subagent_registry.py +151 -0
- minion_code/tools/__init__.py +8 -2
- minion_code/tools/bash_tool.py +16 -3
- minion_code/tools/file_edit_tool.py +201 -104
- minion_code/tools/file_read_tool.py +183 -26
- minion_code/tools/file_write_tool.py +17 -3
- minion_code/tools/glob_tool.py +23 -2
- minion_code/tools/grep_tool.py +229 -21
- minion_code/tools/ls_tool.py +28 -3
- minion_code/tools/multi_edit_tool.py +89 -84
- minion_code/tools/python_interpreter_tool.py +9 -1
- minion_code/tools/skill_tool.py +210 -0
- minion_code/tools/task_tool.py +287 -0
- minion_code/tools/todo_read_tool.py +28 -24
- minion_code/tools/todo_write_tool.py +82 -65
- minion_code/{types.py → type_defs.py} +15 -2
- minion_code/utils/__init__.py +45 -17
- minion_code/utils/config.py +610 -0
- minion_code/utils/history.py +114 -0
- minion_code/utils/logs.py +53 -0
- minion_code/utils/mcp_loader.py +153 -55
- minion_code/utils/output_truncator.py +233 -0
- minion_code/utils/session_storage.py +369 -0
- minion_code/utils/todo_file_utils.py +26 -22
- minion_code/utils/todo_storage.py +43 -33
- minion_code/web/__init__.py +9 -0
- minion_code/web/adapters/__init__.py +5 -0
- minion_code/web/adapters/web_adapter.py +524 -0
- minion_code/web/api/__init__.py +7 -0
- minion_code/web/api/chat.py +277 -0
- minion_code/web/api/interactions.py +136 -0
- minion_code/web/api/sessions.py +135 -0
- minion_code/web/server.py +149 -0
- minion_code/web/services/__init__.py +5 -0
- minion_code/web/services/session_manager.py +420 -0
- minion_code-0.1.1.dist-info/METADATA +475 -0
- minion_code-0.1.1.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/WHEEL +1 -1
- minion_code-0.1.1.dist-info/entry_points.txt +6 -0
- tests/test_adapter.py +67 -0
- tests/test_adapter_simple.py +79 -0
- tests/test_file_read_tool.py +144 -0
- tests/test_readonly_tools.py +0 -2
- tests/test_skills.py +441 -0
- examples/advance_tui.py +0 -508
- examples/rich_example.py +0 -4
- examples/simple_file_watching.py +0 -57
- examples/simple_tui.py +0 -267
- examples/simple_usage.py +0 -69
- minion_code-0.1.0.dist-info/METADATA +0 -350
- minion_code-0.1.0.dist-info/RECORD +0 -59
- minion_code-0.1.0.dist-info/entry_points.txt +0 -4
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Model command - View and configure the LLM model
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import yaml
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
from minion_code.commands import BaseCommand, CommandType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelCommand(BaseCommand):
|
|
15
|
+
"""View or set the default LLM model."""
|
|
16
|
+
|
|
17
|
+
name = "model"
|
|
18
|
+
description = "View or set the default LLM model"
|
|
19
|
+
usage = "/model [model_name] or /model --clear or /model --list"
|
|
20
|
+
aliases = ["llm"]
|
|
21
|
+
command_type = CommandType.LOCAL
|
|
22
|
+
|
|
23
|
+
# Config file path
|
|
24
|
+
CONFIG_DIR = Path.home() / ".minion"
|
|
25
|
+
CONFIG_FILE = CONFIG_DIR / "minion-code.json"
|
|
26
|
+
MINION_CONFIG_FILE = CONFIG_DIR / "config.yaml"
|
|
27
|
+
|
|
28
|
+
def _load_config(self) -> dict:
|
|
29
|
+
"""Load config from file."""
|
|
30
|
+
if self.CONFIG_FILE.exists():
|
|
31
|
+
try:
|
|
32
|
+
with open(self.CONFIG_FILE, "r") as f:
|
|
33
|
+
return json.load(f)
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
def _save_config(self, config: dict) -> None:
|
|
39
|
+
"""Save config to file."""
|
|
40
|
+
self.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
with open(self.CONFIG_FILE, "w") as f:
|
|
42
|
+
json.dump(config, f, indent=2)
|
|
43
|
+
|
|
44
|
+
def _get_available_models(self) -> List[str]:
|
|
45
|
+
"""Get list of available models from minion config.yaml.
|
|
46
|
+
|
|
47
|
+
Uses the same logic as minion's load_config():
|
|
48
|
+
1. First load user config (~/.minion/config.yaml)
|
|
49
|
+
2. Then load base config (MINION_ROOT/config/config.yaml) which OVERWRITES user config
|
|
50
|
+
"""
|
|
51
|
+
config_dict = {}
|
|
52
|
+
|
|
53
|
+
# First load user config
|
|
54
|
+
if self.MINION_CONFIG_FILE.exists():
|
|
55
|
+
try:
|
|
56
|
+
with open(self.MINION_CONFIG_FILE, "r") as f:
|
|
57
|
+
config_dict = yaml.safe_load(f) or {}
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
# Then load base config (MINION_ROOT), which overwrites user config
|
|
62
|
+
try:
|
|
63
|
+
from minion.const import get_minion_root
|
|
64
|
+
|
|
65
|
+
minion_root = get_minion_root()
|
|
66
|
+
base_config_path = Path(minion_root) / "config" / "config.yaml"
|
|
67
|
+
if base_config_path.exists():
|
|
68
|
+
with open(base_config_path, "r") as f:
|
|
69
|
+
base_config = yaml.safe_load(f) or {}
|
|
70
|
+
config_dict.update(base_config) # Overwrite, same as minion
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
models = list(config_dict.get("models", {}).keys())
|
|
75
|
+
return sorted(models)
|
|
76
|
+
|
|
77
|
+
def _update_agent_model(self, model_name: str) -> bool:
|
|
78
|
+
"""Update the agent's model at runtime."""
|
|
79
|
+
if not self.agent:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
from minion.types.llm_types import create_llm_from_model
|
|
84
|
+
|
|
85
|
+
# Create new LLM provider with the new model
|
|
86
|
+
new_llm = create_llm_from_model(model_name)
|
|
87
|
+
self.agent.llm = new_llm
|
|
88
|
+
|
|
89
|
+
# Update llms dict if it exists
|
|
90
|
+
if hasattr(self.agent, "llms"):
|
|
91
|
+
self.agent.llms["main"] = new_llm
|
|
92
|
+
|
|
93
|
+
return True
|
|
94
|
+
except Exception as e:
|
|
95
|
+
self.output.warning(f"Could not update runtime model: {e}")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
def _set_model(self, model_name: str) -> None:
|
|
99
|
+
"""Set the model and update config."""
|
|
100
|
+
config = self._load_config()
|
|
101
|
+
config["model"] = model_name
|
|
102
|
+
self._save_config(config)
|
|
103
|
+
|
|
104
|
+
# Try to update the running agent's model
|
|
105
|
+
runtime_updated = self._update_agent_model(model_name)
|
|
106
|
+
|
|
107
|
+
if runtime_updated:
|
|
108
|
+
self.output.success(f"Model changed to: {model_name}")
|
|
109
|
+
else:
|
|
110
|
+
self.output.success(f"Default model set to: {model_name}")
|
|
111
|
+
self.output.warning("Restart session to use the new model.")
|
|
112
|
+
|
|
113
|
+
self.output.info(f"Config saved to: {self.CONFIG_FILE}")
|
|
114
|
+
|
|
115
|
+
async def execute(self, args: str) -> None:
|
|
116
|
+
"""Execute the model command."""
|
|
117
|
+
args = args.strip()
|
|
118
|
+
config = self._load_config()
|
|
119
|
+
|
|
120
|
+
if args == "--clear" or args == "-c":
|
|
121
|
+
# Clear model setting
|
|
122
|
+
if "model" in config:
|
|
123
|
+
del config["model"]
|
|
124
|
+
self._save_config(config)
|
|
125
|
+
self.output.success("Model setting cleared. Will use default model.")
|
|
126
|
+
else:
|
|
127
|
+
self.output.info("No model setting to clear.")
|
|
128
|
+
|
|
129
|
+
elif args == "--list" or args == "-l":
|
|
130
|
+
# List available models
|
|
131
|
+
models = self._get_available_models()
|
|
132
|
+
if models:
|
|
133
|
+
self.output.info(f"Available models ({len(models)}):")
|
|
134
|
+
for model in models:
|
|
135
|
+
self.output.text(f" - {model}")
|
|
136
|
+
else:
|
|
137
|
+
self.output.warning("No models found in config.yaml")
|
|
138
|
+
|
|
139
|
+
elif args:
|
|
140
|
+
# Set model directly
|
|
141
|
+
self._set_model(args)
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
# Interactive mode: show current model and let user choose
|
|
145
|
+
current_config_model = config.get("model")
|
|
146
|
+
|
|
147
|
+
# Get current agent model
|
|
148
|
+
current_agent_model = None
|
|
149
|
+
if self.agent:
|
|
150
|
+
current_agent_model = getattr(self.agent, "llm", None)
|
|
151
|
+
|
|
152
|
+
# Display current info
|
|
153
|
+
headers = ["Setting", "Value"]
|
|
154
|
+
rows = []
|
|
155
|
+
|
|
156
|
+
if current_agent_model:
|
|
157
|
+
rows.append(["Current Session Model", str(current_agent_model)])
|
|
158
|
+
|
|
159
|
+
if current_config_model:
|
|
160
|
+
rows.append(["Config File Model", current_config_model])
|
|
161
|
+
else:
|
|
162
|
+
rows.append(["Config File Model", "(not set - using default)"])
|
|
163
|
+
|
|
164
|
+
self.output.table(headers, rows, title="Model Configuration")
|
|
165
|
+
|
|
166
|
+
# Get available models and show selection
|
|
167
|
+
models = self._get_available_models()
|
|
168
|
+
if models:
|
|
169
|
+
# Add current model indicator
|
|
170
|
+
choices = []
|
|
171
|
+
for model in models:
|
|
172
|
+
if model == current_config_model:
|
|
173
|
+
choices.append(f"{model} (current)")
|
|
174
|
+
else:
|
|
175
|
+
choices.append(model)
|
|
176
|
+
|
|
177
|
+
# Ask user to select (returns index, -1 if cancelled)
|
|
178
|
+
selected_index = await self.output.choice(
|
|
179
|
+
message="Select a model:", choices=choices, title="Available Models"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if selected_index >= 0 and selected_index < len(models):
|
|
183
|
+
# Get the model name from original models list (without " (current)" suffix)
|
|
184
|
+
model_name = models[selected_index]
|
|
185
|
+
if model_name != current_config_model:
|
|
186
|
+
self._set_model(model_name)
|
|
187
|
+
else:
|
|
188
|
+
self.output.info("Model unchanged.")
|
|
189
|
+
else:
|
|
190
|
+
# No models found, show usage hints
|
|
191
|
+
self.output.info("\nUsage:")
|
|
192
|
+
self.output.text(" /model <name> - Set model (e.g., /model gpt-4o)")
|
|
193
|
+
self.output.text(" /model --list - List available models")
|
|
194
|
+
self.output.text(" /model --clear - Clear saved model setting")
|
|
@@ -4,29 +4,26 @@
|
|
|
4
4
|
Quit command - Exit the application
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
from minion_code.commands import BaseCommand
|
|
7
|
+
from minion_code.commands import BaseCommand, CommandType
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
class QuitCommand(BaseCommand):
|
|
12
11
|
"""Exit the application."""
|
|
13
|
-
|
|
12
|
+
|
|
14
13
|
name = "quit"
|
|
15
14
|
description = "Exit the application"
|
|
16
15
|
usage = "/quit"
|
|
17
16
|
aliases = ["exit", "q", "bye"]
|
|
18
|
-
|
|
17
|
+
command_type = CommandType.LOCAL
|
|
18
|
+
|
|
19
19
|
async def execute(self, args: str) -> None:
|
|
20
20
|
"""Execute the quit command."""
|
|
21
|
-
|
|
22
|
-
"👋
|
|
23
|
-
title="[bold red]Exit[/bold red]",
|
|
24
|
-
border_style="red"
|
|
21
|
+
self.output.panel(
|
|
22
|
+
"👋 Goodbye! Thanks for using MinionCode!", title="Exit", border_style="red"
|
|
25
23
|
)
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
|
|
28
25
|
# Set a flag that the TUI can check and cleanup resources
|
|
29
|
-
if hasattr(self,
|
|
26
|
+
if hasattr(self, "_tui_instance"):
|
|
30
27
|
self._tui_instance.running = False
|
|
31
28
|
# Cleanup MCP resources
|
|
32
|
-
await self._tui_instance.cleanup()
|
|
29
|
+
await self._tui_instance.cleanup()
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Resume command - Resume a previous session
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from minion_code.commands import BaseCommand, CommandType
|
|
9
|
+
from minion_code.utils.session_storage import (
|
|
10
|
+
list_sessions,
|
|
11
|
+
load_session,
|
|
12
|
+
get_latest_session_id,
|
|
13
|
+
SessionMetadata,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ResumeCommand(BaseCommand):
|
|
18
|
+
"""Resume a previous conversation session."""
|
|
19
|
+
|
|
20
|
+
name = "resume"
|
|
21
|
+
description = "Resume a previous session or list available sessions"
|
|
22
|
+
usage = "/resume [session_id] or /resume list"
|
|
23
|
+
aliases = ["r"]
|
|
24
|
+
command_type = CommandType.LOCAL
|
|
25
|
+
|
|
26
|
+
async def execute(self, args: str) -> None:
|
|
27
|
+
"""Execute the resume command.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
args: Optional session_id or 'list' to show available sessions
|
|
31
|
+
"""
|
|
32
|
+
args = args.strip()
|
|
33
|
+
current_project = os.getcwd()
|
|
34
|
+
|
|
35
|
+
# /resume list - show available sessions
|
|
36
|
+
if args.lower() == "list" or args.lower() == "ls":
|
|
37
|
+
await self._list_sessions(current_project)
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# /resume <session_id> - resume specific session
|
|
41
|
+
if args:
|
|
42
|
+
await self._resume_session(args)
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
# /resume (no args) - resume latest session for current project
|
|
46
|
+
latest_id = get_latest_session_id(project_path=current_project)
|
|
47
|
+
if latest_id:
|
|
48
|
+
await self._resume_session(latest_id)
|
|
49
|
+
else:
|
|
50
|
+
self.output.panel(
|
|
51
|
+
"No previous sessions found for this project.\n\n"
|
|
52
|
+
"Use `/resume list` to see all available sessions.",
|
|
53
|
+
title="No Sessions",
|
|
54
|
+
border_style="yellow",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def _list_sessions(self, project_path: str) -> None:
|
|
58
|
+
"""List available sessions."""
|
|
59
|
+
# Get sessions for current project
|
|
60
|
+
project_sessions = list_sessions(project_path=project_path, limit=10)
|
|
61
|
+
|
|
62
|
+
# Get all sessions
|
|
63
|
+
all_sessions = list_sessions(limit=20)
|
|
64
|
+
|
|
65
|
+
if not all_sessions:
|
|
66
|
+
self.output.panel(
|
|
67
|
+
"No saved sessions found.\n\n"
|
|
68
|
+
"Sessions are automatically saved during conversations.",
|
|
69
|
+
title="No Sessions",
|
|
70
|
+
border_style="yellow",
|
|
71
|
+
)
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
# Build output
|
|
75
|
+
lines = []
|
|
76
|
+
|
|
77
|
+
# Current project sessions
|
|
78
|
+
if project_sessions:
|
|
79
|
+
lines.append("**This Project:**")
|
|
80
|
+
lines.append("")
|
|
81
|
+
for session in project_sessions[:5]:
|
|
82
|
+
lines.append(self._format_session_line(session))
|
|
83
|
+
lines.append("")
|
|
84
|
+
|
|
85
|
+
# Other sessions
|
|
86
|
+
other_sessions = [s for s in all_sessions if s.project_path != project_path]
|
|
87
|
+
if other_sessions:
|
|
88
|
+
lines.append("**Other Projects:**")
|
|
89
|
+
lines.append("")
|
|
90
|
+
for session in other_sessions[:5]:
|
|
91
|
+
lines.append(self._format_session_line(session, show_path=True))
|
|
92
|
+
lines.append("")
|
|
93
|
+
|
|
94
|
+
lines.append("---")
|
|
95
|
+
lines.append("Use `/resume <id>` to restore a session")
|
|
96
|
+
lines.append("Use `/resume` to restore the latest session")
|
|
97
|
+
|
|
98
|
+
self.output.panel(
|
|
99
|
+
"\n".join(lines), title="Available Sessions", border_style="blue"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def _format_session_line(
|
|
103
|
+
self, session: SessionMetadata, show_path: bool = False
|
|
104
|
+
) -> str:
|
|
105
|
+
"""Format a session for display."""
|
|
106
|
+
# Parse timestamp for display
|
|
107
|
+
try:
|
|
108
|
+
from datetime import datetime
|
|
109
|
+
|
|
110
|
+
updated = datetime.fromisoformat(session.updated_at)
|
|
111
|
+
time_str = updated.strftime("%m/%d %H:%M")
|
|
112
|
+
except:
|
|
113
|
+
time_str = session.updated_at[:16]
|
|
114
|
+
|
|
115
|
+
title = session.title or "(no title)"
|
|
116
|
+
if len(title) > 40:
|
|
117
|
+
title = title[:40] + "..."
|
|
118
|
+
|
|
119
|
+
line = f" `{session.session_id}` - {title} ({session.message_count} msgs, {time_str})"
|
|
120
|
+
|
|
121
|
+
if show_path:
|
|
122
|
+
# Show shortened path
|
|
123
|
+
path = session.project_path
|
|
124
|
+
home = os.path.expanduser("~")
|
|
125
|
+
if path.startswith(home):
|
|
126
|
+
path = "~" + path[len(home) :]
|
|
127
|
+
if len(path) > 30:
|
|
128
|
+
path = "..." + path[-27:]
|
|
129
|
+
line += f"\n {path}"
|
|
130
|
+
|
|
131
|
+
return line
|
|
132
|
+
|
|
133
|
+
async def _resume_session(self, session_id: str) -> None:
|
|
134
|
+
"""Resume a specific session."""
|
|
135
|
+
session = load_session(session_id)
|
|
136
|
+
|
|
137
|
+
if not session:
|
|
138
|
+
self.output.panel(
|
|
139
|
+
f"Session `{session_id}` not found.\n\n"
|
|
140
|
+
"Use `/resume list` to see available sessions.",
|
|
141
|
+
title="Session Not Found",
|
|
142
|
+
border_style="red",
|
|
143
|
+
)
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
# Check if agent supports session restoration
|
|
147
|
+
if not self.agent:
|
|
148
|
+
self.output.panel(
|
|
149
|
+
"Agent not initialized. Cannot restore session.",
|
|
150
|
+
title="Error",
|
|
151
|
+
border_style="red",
|
|
152
|
+
)
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
# Check if agent has restore_session method
|
|
156
|
+
if not hasattr(self.agent, "restore_session"):
|
|
157
|
+
self.output.panel(
|
|
158
|
+
"Session restoration not yet implemented in agent.\n\n"
|
|
159
|
+
f"Session `{session_id}` has {len(session.messages)} messages:\n"
|
|
160
|
+
f"Title: {session.metadata.title or '(none)'}\n"
|
|
161
|
+
f"Project: {session.metadata.project_path}",
|
|
162
|
+
title="Coming Soon",
|
|
163
|
+
border_style="yellow",
|
|
164
|
+
)
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Restore the session
|
|
168
|
+
try:
|
|
169
|
+
await self.agent.restore_session(session)
|
|
170
|
+
|
|
171
|
+
self.output.panel(
|
|
172
|
+
f"Restored session `{session_id}` with {len(session.messages)} messages.\n\n"
|
|
173
|
+
f"Title: {session.metadata.title or '(none)'}\n"
|
|
174
|
+
f"You can continue the conversation now.",
|
|
175
|
+
title="Session Restored",
|
|
176
|
+
border_style="green",
|
|
177
|
+
)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
self.output.panel(
|
|
180
|
+
f"Failed to restore session: {e}", title="Error", border_style="red"
|
|
181
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Skill command - wrapper to execute skills as slash commands.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from . import BaseCommand, CommandType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SkillCommand(BaseCommand):
|
|
11
|
+
"""
|
|
12
|
+
Command wrapper for executing skills.
|
|
13
|
+
|
|
14
|
+
Skills are dynamically loaded and can be executed like /skill-name.
|
|
15
|
+
This is a PROMPT type command - the skill content is expanded
|
|
16
|
+
and sent to the LLM.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
name: str = "" # Set dynamically
|
|
20
|
+
description: str = "" # Set dynamically
|
|
21
|
+
usage: str = "" # Set dynamically
|
|
22
|
+
command_type: CommandType = CommandType.PROMPT
|
|
23
|
+
is_skill: bool = True
|
|
24
|
+
|
|
25
|
+
def __init__(self, output, agent=None, skill=None):
|
|
26
|
+
"""
|
|
27
|
+
Initialize the skill command.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
output: OutputAdapter instance
|
|
31
|
+
agent: Optional agent instance
|
|
32
|
+
skill: Skill object to wrap
|
|
33
|
+
"""
|
|
34
|
+
super().__init__(output, agent)
|
|
35
|
+
self.skill = skill
|
|
36
|
+
|
|
37
|
+
if skill:
|
|
38
|
+
self.name = skill.name
|
|
39
|
+
self.description = skill.description
|
|
40
|
+
self.usage = f"/{skill.name}"
|
|
41
|
+
|
|
42
|
+
async def execute(self, args: str) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Execute is not used for PROMPT commands.
|
|
45
|
+
The get_prompt method is used instead.
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
async def get_prompt(self, args: str) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Get the expanded prompt for this skill.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
args: Command arguments (usually ignored for skills)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Expanded prompt string containing skill instructions
|
|
58
|
+
"""
|
|
59
|
+
if not self.skill:
|
|
60
|
+
return f"Error: Skill not found"
|
|
61
|
+
|
|
62
|
+
# Build the skill activation message
|
|
63
|
+
header = f'<command-message>The "{self.skill.name}" skill is loading</command-message>'
|
|
64
|
+
skill_content = self.skill.get_prompt()
|
|
65
|
+
|
|
66
|
+
return f"{header}\n\n{skill_content}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def create_skill_command(skill) -> type:
|
|
70
|
+
"""
|
|
71
|
+
Dynamically create a SkillCommand class for a specific skill.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
skill: Skill object
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
SkillCommand subclass configured for the skill
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
class DynamicSkillCommand(SkillCommand):
|
|
81
|
+
name = skill.name
|
|
82
|
+
description = skill.description
|
|
83
|
+
usage = f"/{skill.name}"
|
|
84
|
+
|
|
85
|
+
def __init__(self, output, agent=None):
|
|
86
|
+
super().__init__(output, agent, skill=skill)
|
|
87
|
+
|
|
88
|
+
DynamicSkillCommand.__name__ = f"{skill.name.replace('-', '_').title()}SkillCommand"
|
|
89
|
+
return DynamicSkillCommand
|
|
@@ -4,112 +4,87 @@
|
|
|
4
4
|
Status command - Show system status and information
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from rich.panel import Panel
|
|
8
|
-
from rich.table import Table
|
|
9
|
-
from rich.text import Text
|
|
10
7
|
import sys
|
|
11
8
|
import platform
|
|
12
9
|
from datetime import datetime
|
|
13
|
-
from minion_code.commands import BaseCommand
|
|
10
|
+
from minion_code.commands import BaseCommand, CommandType
|
|
14
11
|
|
|
15
12
|
|
|
16
13
|
class StatusCommand(BaseCommand):
|
|
17
14
|
"""Show system status and information."""
|
|
18
|
-
|
|
15
|
+
|
|
19
16
|
name = "status"
|
|
20
17
|
description = "Show system status, agent info, and statistics"
|
|
21
18
|
usage = "/status"
|
|
22
19
|
aliases = ["info", "stat"]
|
|
23
|
-
|
|
20
|
+
command_type = CommandType.LOCAL
|
|
21
|
+
|
|
24
22
|
async def execute(self, args: str) -> None:
|
|
25
23
|
"""Execute the status command."""
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
header_style="bold blue"
|
|
31
|
-
)
|
|
32
|
-
status_table.add_column("Component", style="cyan", no_wrap=True)
|
|
33
|
-
status_table.add_column("Status", style="white")
|
|
34
|
-
status_table.add_column("Details", style="yellow")
|
|
35
|
-
|
|
24
|
+
# Prepare table data
|
|
25
|
+
headers = ["Component", "Status", "Details"]
|
|
26
|
+
rows = []
|
|
27
|
+
|
|
36
28
|
# System info
|
|
37
|
-
|
|
38
|
-
"System",
|
|
39
|
-
"✅ Running",
|
|
40
|
-
f"{platform.system()} {platform.release()}"
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
status_table.add_row(
|
|
44
|
-
"Python",
|
|
45
|
-
"✅ Active",
|
|
46
|
-
f"{sys.version.split()[0]}"
|
|
29
|
+
rows.append(
|
|
30
|
+
["System", "✅ Running", f"{platform.system()} {platform.release()}"]
|
|
47
31
|
)
|
|
48
|
-
|
|
32
|
+
|
|
33
|
+
rows.append(["Python", "✅ Active", f"{sys.version.split()[0]}"])
|
|
34
|
+
|
|
49
35
|
# Agent info
|
|
50
36
|
if self.agent:
|
|
51
37
|
history = self.agent.get_conversation_history()
|
|
52
38
|
tools_count = len(self.agent.tools) if self.agent.tools else 0
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"📝 Active" if history else "📝 Empty",
|
|
63
|
-
f"{len(history)} messages" if history else "No messages"
|
|
39
|
+
|
|
40
|
+
rows.append(["Agent", "✅ Ready", f"{tools_count} tools loaded"])
|
|
41
|
+
|
|
42
|
+
rows.append(
|
|
43
|
+
[
|
|
44
|
+
"Conversation",
|
|
45
|
+
"📝 Active" if history else "📝 Empty",
|
|
46
|
+
f"{len(history)} messages" if history else "No messages",
|
|
47
|
+
]
|
|
64
48
|
)
|
|
65
49
|
else:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"❌ Not Ready",
|
|
69
|
-
"Not initialized"
|
|
70
|
-
)
|
|
71
|
-
|
|
50
|
+
rows.append(["Agent", "❌ Not Ready", "Not initialized"])
|
|
51
|
+
|
|
72
52
|
# Memory info (basic)
|
|
73
53
|
try:
|
|
74
54
|
import psutil
|
|
55
|
+
|
|
75
56
|
memory = psutil.virtual_memory()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
57
|
+
rows.append(
|
|
58
|
+
[
|
|
59
|
+
"Memory",
|
|
60
|
+
"📊 Monitored",
|
|
61
|
+
f"{memory.percent}% used ({memory.available // (1024**3)} GB free)",
|
|
62
|
+
]
|
|
80
63
|
)
|
|
81
64
|
except ImportError:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
self.console.print(status_table)
|
|
89
|
-
|
|
65
|
+
rows.append(["Memory", "❌ Not Available", "psutil not installed"])
|
|
66
|
+
|
|
67
|
+
# Display table
|
|
68
|
+
self.output.table(headers, rows, title="📊 System Status")
|
|
69
|
+
|
|
90
70
|
# Additional info panel
|
|
91
71
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
info_text.append(f"{current_time}\n", style="white")
|
|
96
|
-
|
|
72
|
+
|
|
73
|
+
info_lines = [f"🕒 Current Time: {current_time}"]
|
|
74
|
+
|
|
97
75
|
if self.agent:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
76
|
+
info_lines.append(
|
|
77
|
+
f"🤖 Agent Model: {getattr(self.agent, 'llm', 'Unknown')}"
|
|
78
|
+
)
|
|
79
|
+
|
|
102
80
|
if self.agent.tools:
|
|
103
81
|
tool_names = [tool.name for tool in self.agent.tools[:5]]
|
|
104
82
|
if len(self.agent.tools) > 5:
|
|
105
83
|
tool_names.append(f"... and {len(self.agent.tools) - 5} more")
|
|
106
|
-
|
|
84
|
+
info_lines.append(f"🛠️ Available Tools: {', '.join(tool_names)}")
|
|
107
85
|
else:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
title="[bold green]Additional Information[/bold green]",
|
|
113
|
-
border_style="green"
|
|
86
|
+
info_lines.append("🛠️ Available Tools: None")
|
|
87
|
+
|
|
88
|
+
self.output.panel(
|
|
89
|
+
"\n".join(info_lines), title="Additional Information", border_style="green"
|
|
114
90
|
)
|
|
115
|
-
self.console.print(info_panel)
|