hanuscode 1.0.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.
- hanus/__init__.py +5 -0
- hanus/__main__.py +10 -0
- hanus/action_handlers.py +76 -0
- hanus/action_parser.py +82 -0
- hanus/agent_runner.py +1445 -0
- hanus/analysis/__init__.py +5 -0
- hanus/analysis/debt.py +702 -0
- hanus/analysis/dependencies.py +475 -0
- hanus/cache/__init__.py +5 -0
- hanus/cache/response_cache.py +560 -0
- hanus/config.py +401 -0
- hanus/connectors/__init__.py +19 -0
- hanus/connectors/base.py +114 -0
- hanus/connectors/claude_connector.py +146 -0
- hanus/connectors/gemini_connector.py +141 -0
- hanus/connectors/glm_connector.py +160 -0
- hanus/connectors/ollama_connector.py +174 -0
- hanus/connectors/openai_connector.py +122 -0
- hanus/connectors/registry.py +26 -0
- hanus/context/__init__.py +7 -0
- hanus/context/manager.py +837 -0
- hanus/context/selective.py +626 -0
- hanus/error_recovery/__init__.py +5 -0
- hanus/error_recovery/auto_fix.py +605 -0
- hanus/hooks/__init__.py +5 -0
- hanus/hooks/manager.py +247 -0
- hanus/instincts/__init__.py +44 -0
- hanus/instincts/cli.py +372 -0
- hanus/instincts/detector.py +281 -0
- hanus/instincts/evolver.py +361 -0
- hanus/instincts/manager.py +343 -0
- hanus/instincts/types.py +253 -0
- hanus/logger.py +81 -0
- hanus/memory/__init__.py +8 -0
- hanus/memory/manager.py +265 -0
- hanus/memory/types.py +119 -0
- hanus/monitor.py +341 -0
- hanus/parallel/__init__.py +5 -0
- hanus/parallel/executor.py +300 -0
- hanus/permissions.py +182 -0
- hanus/plan/__init__.py +8 -0
- hanus/plan/mode.py +267 -0
- hanus/plan/models.py +152 -0
- hanus/plugin_manager.py +754 -0
- hanus/plugin_registry.py +391 -0
- hanus/plugins/__init__.py +1 -0
- hanus/plugins/arena.py +630 -0
- hanus/plugins/code_review.py +123 -0
- hanus/plugins/cortex.py +1750 -0
- hanus/plugins/deps_check.py +27 -0
- hanus/plugins/git_ops.py +33 -0
- hanus/plugins/metasploit.py +530 -0
- hanus/plugins/notes.py +583 -0
- hanus/plugins/search_code.py +59 -0
- hanus/plugins/searchsploit.py +495 -0
- hanus/plugins/strategist.py +175 -0
- hanus/plugins/webui.py +5200 -0
- hanus/profiles.py +479 -0
- hanus/profiles_builtin/__init__.py +0 -0
- hanus/profiles_builtin/architect/profile.yaml +12 -0
- hanus/profiles_builtin/architect/system_prompt.txt +71 -0
- hanus/profiles_builtin/deep/profile.yaml +12 -0
- hanus/profiles_builtin/deep/system_prompt.txt +66 -0
- hanus/profiles_builtin/developer/__init__.py +0 -0
- hanus/profiles_builtin/developer/profile.yaml +9 -0
- hanus/profiles_builtin/developer/system_prompt.txt +176 -0
- hanus/profiles_builtin/speed/profile.yaml +12 -0
- hanus/profiles_builtin/speed/system_prompt.txt +51 -0
- hanus/project_tools.py +177 -0
- hanus/query_engine.py +1594 -0
- hanus/rules/__init__.py +237 -0
- hanus/search/__init__.py +5 -0
- hanus/search/semantic.py +596 -0
- hanus/session_manager.py +547 -0
- hanus/skill_manager.py +702 -0
- hanus/skills/__init__.py +4 -0
- hanus/subagent/__init__.py +8 -0
- hanus/subagent/agents/__init__.py +253 -0
- hanus/subagent/manager.py +309 -0
- hanus/subagent/types.py +266 -0
- hanus/suggestions/__init__.py +5 -0
- hanus/suggestions/proactive.py +451 -0
- hanus/tasks/__init__.py +8 -0
- hanus/tasks/manager.py +330 -0
- hanus/tasks/models.py +106 -0
- hanus/terminal_prompt.py +166 -0
- hanus/tools.py +1849 -0
- hanus/ui.py +939 -0
- hanuscode-1.0.0.dist-info/METADATA +1151 -0
- hanuscode-1.0.0.dist-info/RECORD +93 -0
- hanuscode-1.0.0.dist-info/WHEEL +5 -0
- hanuscode-1.0.0.dist-info/entry_points.txt +2 -0
- hanuscode-1.0.0.dist-info/top_level.txt +1 -0
hanus/agent_runner.py
ADDED
|
@@ -0,0 +1,1445 @@
|
|
|
1
|
+
# hanus/agent_runner.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import sys
|
|
4
|
+
import textwrap as _tw
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from colorama import init as _cinit
|
|
10
|
+
_cinit(autoreset=True)
|
|
11
|
+
except ImportError:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from hanus.config import HanusConfig
|
|
16
|
+
from hanus.ui import UI, _strip_xml, W, P
|
|
17
|
+
from hanus.permissions import PermissionManager, PermissionMode
|
|
18
|
+
from hanus.session_manager import SessionManager
|
|
19
|
+
from hanus.query_engine import QueryEngine
|
|
20
|
+
from hanus.plugin_manager import PluginManager
|
|
21
|
+
from hanus.tools import ToolExecutor
|
|
22
|
+
from hanus.profiles import ProfileManager
|
|
23
|
+
from hanus.tasks import TaskManager
|
|
24
|
+
from hanus.memory import MemoryManager
|
|
25
|
+
from hanus.context import ContextManager
|
|
26
|
+
from hanus.subagent import SubagentManager
|
|
27
|
+
from hanus.plan import PlanMode
|
|
28
|
+
from hanus.skill_manager import get_skill_manager
|
|
29
|
+
from hanus.monitor import get_monitor_manager
|
|
30
|
+
import hanus.connectors
|
|
31
|
+
import hanus.logger as log
|
|
32
|
+
from hanus.connectors.registry import ConnectorRegistry
|
|
33
|
+
from hanus.project_tools import list_files, build_context_from_folder
|
|
34
|
+
except ImportError as e:
|
|
35
|
+
print(f"\033[91m[ERROR] Import error: {e}\033[0m")
|
|
36
|
+
print()
|
|
37
|
+
print("\033[93mTo install from GitHub:\033[0m")
|
|
38
|
+
print(" git clone https://github.com/hanuscode/hanuscode.git")
|
|
39
|
+
print(" cd hanuscode")
|
|
40
|
+
print(" pip install -e .")
|
|
41
|
+
print()
|
|
42
|
+
print("\033[93mOr if already cloned, from the project directory:\033[0m")
|
|
43
|
+
print(" pip install -e .")
|
|
44
|
+
print()
|
|
45
|
+
print("\033[93mBasic dependencies:\033[0m")
|
|
46
|
+
print(" pip install colorama requests pyyaml")
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
def _build_context(config):
|
|
53
|
+
return build_context_from_folder(
|
|
54
|
+
str(config.root_dir),
|
|
55
|
+
max_files=config.context_max_files,
|
|
56
|
+
include_content=config.context_include_content,
|
|
57
|
+
content_preview_chars=config.context_preview_chars,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _make_connector(config, ui):
|
|
62
|
+
try:
|
|
63
|
+
return ConnectorRegistry.get(config.provider, config.get_connector_config())
|
|
64
|
+
except Exception as e:
|
|
65
|
+
ui.error(f"Could not create connector '{config.provider}': {e}")
|
|
66
|
+
ui.info(f"Available: {ConnectorRegistry.available()}")
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _autosave(config, session_mgr, engine, ui, force=False):
|
|
71
|
+
if (config.auto_save_session or force) and session_mgr.current:
|
|
72
|
+
session_mgr.current.messages = engine.get_messages()
|
|
73
|
+
session_mgr.save()
|
|
74
|
+
if force:
|
|
75
|
+
ui.success("Session saved.")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _show_response(response, ui):
|
|
79
|
+
if not response.text:
|
|
80
|
+
return
|
|
81
|
+
ui.stream_start()
|
|
82
|
+
clean = _strip_xml(response.text)
|
|
83
|
+
for line in (clean.strip().splitlines() or [""]):
|
|
84
|
+
if line.strip():
|
|
85
|
+
for wl in (_tw.wrap(line, W - 4) or [line]):
|
|
86
|
+
print(f" {wl}")
|
|
87
|
+
else:
|
|
88
|
+
print()
|
|
89
|
+
ui.stream_end()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _cmd_exact(s: str, target: str) -> bool:
|
|
93
|
+
return s.lower().strip() == target
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _cmd_is(s: str, target: str) -> bool:
|
|
97
|
+
lower = s.lower().strip()
|
|
98
|
+
return lower == target or lower.startswith(target + " ")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _make_engine(config, connector, executor, session_mgr, plugin_mgr, ui, context_mgr=None):
|
|
102
|
+
return QueryEngine(
|
|
103
|
+
connector=connector,
|
|
104
|
+
tool_executor=executor,
|
|
105
|
+
session_manager=session_mgr,
|
|
106
|
+
permission_manager=executor.perms,
|
|
107
|
+
plugin_manager=plugin_mgr,
|
|
108
|
+
stream_callback=ui.stream_token,
|
|
109
|
+
tool_start_callback=ui.tool_start,
|
|
110
|
+
tool_end_callback=ui.tool_end,
|
|
111
|
+
thinking_callback=ui.thinking,
|
|
112
|
+
budget_usd=config.budget_usd,
|
|
113
|
+
context_manager=context_mgr,
|
|
114
|
+
ui=ui,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ─── Main ─────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
def main():
|
|
121
|
+
"""Entry point principal - detecta argumentos CLI."""
|
|
122
|
+
cli_main()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cli_main():
|
|
126
|
+
"""
|
|
127
|
+
CLI entry point with argument handling.
|
|
128
|
+
Supports both interactive and non-interactive modes.
|
|
129
|
+
"""
|
|
130
|
+
import argparse
|
|
131
|
+
|
|
132
|
+
parser = argparse.ArgumentParser(
|
|
133
|
+
prog="hanuscode",
|
|
134
|
+
description="Autonomous Programming Agent for Terminal and Web",
|
|
135
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
136
|
+
epilog="""
|
|
137
|
+
Examples:
|
|
138
|
+
hanuscode # Interactive mode in current directory
|
|
139
|
+
hanuscode --cmd "Analyze the code" # Execute command and exit
|
|
140
|
+
hanuscode -c "Create a README" # Short form
|
|
141
|
+
hanuscode --path /project --cmd "..." # In specific directory
|
|
142
|
+
"""
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
parser.add_argument("--cmd", "-c", type=str, default=None,
|
|
146
|
+
help="Command to execute (exits after completion)")
|
|
147
|
+
parser.add_argument("--path", "-p", type=str, default=None,
|
|
148
|
+
help="Working directory (default: current directory)")
|
|
149
|
+
parser.add_argument("--profile", type=str, default=None,
|
|
150
|
+
help="Profile to use: developer, security, auditor, architect")
|
|
151
|
+
parser.add_argument("--model", type=str, default=None,
|
|
152
|
+
help="Model to use: provider/model")
|
|
153
|
+
parser.add_argument("--mode", type=str, default=None,
|
|
154
|
+
choices=["default", "plan", "bypass"],
|
|
155
|
+
help="Permission mode: default, plan, bypass")
|
|
156
|
+
parser.add_argument("--version", "-v", action="store_true",
|
|
157
|
+
help="Show version")
|
|
158
|
+
|
|
159
|
+
args = parser.parse_args()
|
|
160
|
+
|
|
161
|
+
# Show version
|
|
162
|
+
if args.version:
|
|
163
|
+
print("hanuscode 1.0.0")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
# Change directory if specified
|
|
167
|
+
if args.path:
|
|
168
|
+
path = Path(args.path).expanduser().resolve()
|
|
169
|
+
if not path.exists():
|
|
170
|
+
print(f"Error: Directory not found: {path}")
|
|
171
|
+
return
|
|
172
|
+
import os
|
|
173
|
+
os.chdir(path)
|
|
174
|
+
|
|
175
|
+
# Execute
|
|
176
|
+
run_with_args(
|
|
177
|
+
cmd=args.cmd,
|
|
178
|
+
profile=args.profile,
|
|
179
|
+
model=args.model,
|
|
180
|
+
mode=args.mode
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def run_with_args(cmd: str = None, profile: str = None, model: str = None, mode: str = None):
|
|
185
|
+
"""
|
|
186
|
+
Execute hanuscode with CLI arguments.
|
|
187
|
+
If `cmd` is passed, executes the command and exits (non-interactive mode).
|
|
188
|
+
"""
|
|
189
|
+
from hanus.terminal_prompt import get_prompt, restore_terminal
|
|
190
|
+
|
|
191
|
+
cwd = Path.cwd()
|
|
192
|
+
config = HanusConfig.load(project_dir=cwd)
|
|
193
|
+
ui = UI()
|
|
194
|
+
|
|
195
|
+
# Aplicar argumentos CLI
|
|
196
|
+
if mode:
|
|
197
|
+
try:
|
|
198
|
+
config.permission_mode = PermissionMode(mode.lower()).value
|
|
199
|
+
except ValueError:
|
|
200
|
+
ui.error(f"Invalid mode: {mode}. Options: default, plan, bypass")
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
if model:
|
|
204
|
+
parts = model.split("/", 1)
|
|
205
|
+
if len(parts) == 2:
|
|
206
|
+
config.provider = parts[0].lower()
|
|
207
|
+
config.model_id = parts[1]
|
|
208
|
+
else:
|
|
209
|
+
config.model_id = model
|
|
210
|
+
|
|
211
|
+
# ── Profiles ──────────────────────────────────────────────────────────────
|
|
212
|
+
profile_mgr = ProfileManager()
|
|
213
|
+
|
|
214
|
+
# If profile specified via CLI, use it
|
|
215
|
+
if profile:
|
|
216
|
+
requested = profile_mgr.get(profile)
|
|
217
|
+
if requested:
|
|
218
|
+
profile_mgr.set_active(profile)
|
|
219
|
+
active_profile = requested
|
|
220
|
+
profile_mgr.apply_to_config(active_profile, config)
|
|
221
|
+
else:
|
|
222
|
+
ui.error(f"Profile not found: {profile}")
|
|
223
|
+
ui.info(f"Available: {[p.name for p in profile_mgr.list_profiles()]}")
|
|
224
|
+
sys.exit(1)
|
|
225
|
+
else:
|
|
226
|
+
active_profile = profile_mgr.get_active()
|
|
227
|
+
if active_profile:
|
|
228
|
+
profile_mgr.apply_to_config(active_profile, config)
|
|
229
|
+
|
|
230
|
+
# ── Non-interactive mode (--cmd) ────────────────────────────────────────────
|
|
231
|
+
if cmd:
|
|
232
|
+
_run_single_command(cmd, config, ui, active_profile, profile_mgr, cwd)
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
# ── Normal interactive mode ────────────────────────────────────────────────
|
|
236
|
+
tp = get_prompt()
|
|
237
|
+
try:
|
|
238
|
+
_main_loop()
|
|
239
|
+
finally:
|
|
240
|
+
restore_terminal()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _run_single_command(cmd: str, config, ui, active_profile, profile_mgr, cwd):
|
|
244
|
+
"""Execute a single command and exit (non-interactive mode)."""
|
|
245
|
+
# ── Permissions ──────────────────────────────────────────────────────────────
|
|
246
|
+
perm_mode = PermissionMode(config.permission_mode)
|
|
247
|
+
perms = PermissionManager(mode=perm_mode)
|
|
248
|
+
# In CMD mode, approve everything automatically
|
|
249
|
+
perms.set_mode(PermissionMode.BYPASS)
|
|
250
|
+
|
|
251
|
+
# ── Task Manager ──────────────────────────────────────────────────────────
|
|
252
|
+
task_mgr = TaskManager(cwd)
|
|
253
|
+
|
|
254
|
+
# ── Memory Manager ────────────────────────────────────────────────────────
|
|
255
|
+
memory_mgr = MemoryManager(cwd)
|
|
256
|
+
|
|
257
|
+
# ── Context Manager ───────────────────────────────────────────────────────
|
|
258
|
+
# Usar configuración de ventana de contexto, con fallback a 200k tokens
|
|
259
|
+
context_window = config.context_window if config.context_window > 0 else 200000
|
|
260
|
+
context_mgr = ContextManager(
|
|
261
|
+
max_tokens=context_window,
|
|
262
|
+
preserve_recent=max(10, context_window // 5000) # Preservar más mensajes en contextos grandes
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
executor = ToolExecutor(cwd, perms, task_manager=task_mgr)
|
|
266
|
+
executor.perms = perms
|
|
267
|
+
executor.memory = memory_mgr
|
|
268
|
+
|
|
269
|
+
# Callback for ask_user (in non-interactive mode, use default)
|
|
270
|
+
def ask_user_callback(question: str, header: str, options: list, multi_select: bool) -> dict:
|
|
271
|
+
# In CMD mode, select the first option by default
|
|
272
|
+
if options:
|
|
273
|
+
return {"answers": {question: options[0]}}
|
|
274
|
+
return {}
|
|
275
|
+
|
|
276
|
+
executor.ask_user_callback = ask_user_callback
|
|
277
|
+
|
|
278
|
+
session_mgr = SessionManager()
|
|
279
|
+
session_mgr.new_session(str(cwd), config.provider, config.model_id)
|
|
280
|
+
|
|
281
|
+
# ── Plugins ───────────────────────────────────────────────────────────────
|
|
282
|
+
pkg_plugins = Path(__file__).parent / "plugins"
|
|
283
|
+
plugins_local = cwd / "plugins"
|
|
284
|
+
plugin_mgr = PluginManager(pkg_plugins) if pkg_plugins.exists() else PluginManager(plugins_local)
|
|
285
|
+
if plugins_local.exists() and plugins_local.is_dir():
|
|
286
|
+
local_mgr = PluginManager(plugins_local)
|
|
287
|
+
for name, plugin in local_mgr.plugins.items():
|
|
288
|
+
plugin_mgr.plugins[name] = plugin
|
|
289
|
+
|
|
290
|
+
connector = _make_connector(config, ui)
|
|
291
|
+
|
|
292
|
+
# ── Subagent Manager ──────────────────────────────────────────────────────
|
|
293
|
+
subagent_mgr = SubagentManager(
|
|
294
|
+
connector=connector,
|
|
295
|
+
tool_executor=executor,
|
|
296
|
+
session_manager=session_mgr,
|
|
297
|
+
permission_manager=perms,
|
|
298
|
+
root_dir=cwd,
|
|
299
|
+
)
|
|
300
|
+
executor.subagents = subagent_mgr
|
|
301
|
+
|
|
302
|
+
# ── Plan Mode ─────────────────────────────────────────────────────────────
|
|
303
|
+
plan_mode = PlanMode(cwd)
|
|
304
|
+
executor.plan_mode = plan_mode
|
|
305
|
+
|
|
306
|
+
# ── Skill Manager ────────────────────────────────────────────────────────
|
|
307
|
+
skill_mgr = get_skill_manager()
|
|
308
|
+
|
|
309
|
+
# ── Monitor Manager ───────────────────────────────────────────────────────
|
|
310
|
+
monitor_mgr = get_monitor_manager()
|
|
311
|
+
|
|
312
|
+
# ── Engine ────────────────────────────────────────────────────────────────
|
|
313
|
+
engine = QueryEngine(
|
|
314
|
+
connector=connector,
|
|
315
|
+
tool_executor=executor,
|
|
316
|
+
session_manager=session_mgr,
|
|
317
|
+
permission_manager=perms,
|
|
318
|
+
plugin_manager=plugin_mgr,
|
|
319
|
+
stream_callback=ui.stream_token,
|
|
320
|
+
tool_start_callback=ui.tool_start,
|
|
321
|
+
tool_end_callback=ui.tool_end,
|
|
322
|
+
thinking_callback=ui.thinking,
|
|
323
|
+
budget_usd=config.budget_usd,
|
|
324
|
+
context_manager=context_mgr,
|
|
325
|
+
ui=ui,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# ── System prompt ─────────────────────────────────────────────────────────
|
|
329
|
+
plugin_docs = plugin_mgr.get_plugin_docs()
|
|
330
|
+
if active_profile:
|
|
331
|
+
system_prompt = active_profile.system_prompt
|
|
332
|
+
if plugin_docs:
|
|
333
|
+
system_prompt += f"\n\n## Available Plugins\n\n{plugin_docs}"
|
|
334
|
+
else:
|
|
335
|
+
system_prompt = config.load_system_prompt(plugin_docs)
|
|
336
|
+
|
|
337
|
+
engine.set_system_prompt(system_prompt)
|
|
338
|
+
|
|
339
|
+
# Load memory
|
|
340
|
+
memory_context = memory_mgr.load_memories()
|
|
341
|
+
if memory_context:
|
|
342
|
+
engine._messages.append({"role": "user", "content": memory_context})
|
|
343
|
+
engine._messages.append({"role": "assistant", "content": "Memory loaded."})
|
|
344
|
+
|
|
345
|
+
# Banner and info
|
|
346
|
+
ui.banner()
|
|
347
|
+
ml = "native" if connector.NATIVE_TOOLS else "XML"
|
|
348
|
+
profile_name = active_profile.meta.display if active_profile else "no profile"
|
|
349
|
+
ui.info(f"Profile: {profile_name} | {connector.provider_name} | Tools: {ml} | Dir: {cwd}")
|
|
350
|
+
|
|
351
|
+
context = _build_context(config)
|
|
352
|
+
engine.inject_context(context)
|
|
353
|
+
|
|
354
|
+
# ── Check if command is a plugin command ─────────────────────────────────────
|
|
355
|
+
if cmd.startswith("/"):
|
|
356
|
+
parts = cmd[1:].split(maxsplit=1)
|
|
357
|
+
plugin_name = parts[0].lower() if parts else ""
|
|
358
|
+
plugin_args = parts[1] if len(parts) > 1 else ""
|
|
359
|
+
if plugin_name in plugin_mgr.plugins:
|
|
360
|
+
result = plugin_mgr.run(plugin_name, plugin_args)
|
|
361
|
+
if result:
|
|
362
|
+
ui.plugin_result(plugin_name, result)
|
|
363
|
+
ui.goodbye()
|
|
364
|
+
sys.exit(0)
|
|
365
|
+
|
|
366
|
+
# ── Execute command ──────────────────────────────────────────────────────
|
|
367
|
+
ui.info(f"Executing: {cmd[:80]}{'...' if len(cmd) > 80 else ''}")
|
|
368
|
+
print()
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
response = engine.send(cmd)
|
|
372
|
+
|
|
373
|
+
if response.text:
|
|
374
|
+
_show_response(response, ui)
|
|
375
|
+
|
|
376
|
+
if config.show_cost and session_mgr.current:
|
|
377
|
+
ui.show_cost_bar(
|
|
378
|
+
response.cost_usd, response.input_tokens, response.output_tokens,
|
|
379
|
+
session_mgr.current.total_cost_usd, config.budget_usd,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Save session
|
|
383
|
+
if config.auto_save_session and session_mgr.current:
|
|
384
|
+
session_mgr.current.messages = engine.get_messages()
|
|
385
|
+
session_mgr.save()
|
|
386
|
+
|
|
387
|
+
print()
|
|
388
|
+
ui.success("Command completed.")
|
|
389
|
+
ui.goodbye()
|
|
390
|
+
sys.exit(0)
|
|
391
|
+
|
|
392
|
+
except KeyboardInterrupt:
|
|
393
|
+
ui.warning("\nInterrupted by user.")
|
|
394
|
+
sys.exit(130)
|
|
395
|
+
|
|
396
|
+
except Exception as e:
|
|
397
|
+
import traceback
|
|
398
|
+
ui.error(f"Error: {e}")
|
|
399
|
+
log.log_error("cmd mode", e)
|
|
400
|
+
if config.verbose:
|
|
401
|
+
print(traceback.format_exc())
|
|
402
|
+
sys.exit(1)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _main_loop():
|
|
406
|
+
"""Main program logic (interactive mode)."""
|
|
407
|
+
from hanus.terminal_prompt import get_prompt
|
|
408
|
+
|
|
409
|
+
cwd = Path.cwd()
|
|
410
|
+
config = HanusConfig.load(project_dir=cwd)
|
|
411
|
+
ui = UI()
|
|
412
|
+
|
|
413
|
+
# ── Profiles ──────────────────────────────────────────────────────────────
|
|
414
|
+
profile_mgr = ProfileManager()
|
|
415
|
+
active_profile = profile_mgr.get_active()
|
|
416
|
+
|
|
417
|
+
if active_profile:
|
|
418
|
+
# Apply profile overrides to config
|
|
419
|
+
profile_mgr.apply_to_config(active_profile, config)
|
|
420
|
+
log.info(f"Active profile: {active_profile.name}")
|
|
421
|
+
|
|
422
|
+
# ── Permissions ──────────────────────────────────────────────────────────────
|
|
423
|
+
perm_mode = PermissionMode(config.permission_mode)
|
|
424
|
+
perms = PermissionManager(mode=perm_mode)
|
|
425
|
+
perms.set_ask_callback(ui.ask_permission)
|
|
426
|
+
|
|
427
|
+
# ── Task Manager ──────────────────────────────────────────────────────────
|
|
428
|
+
task_mgr = TaskManager(config.root_dir)
|
|
429
|
+
|
|
430
|
+
# ── Memory Manager ────────────────────────────────────────────────────────
|
|
431
|
+
memory_mgr = MemoryManager(config.root_dir)
|
|
432
|
+
|
|
433
|
+
# ── Context Manager (context compression) ──────────────────────────────────
|
|
434
|
+
# Usar configuración de ventana de contexto, con fallback a 200k tokens
|
|
435
|
+
context_window = config.context_window if config.context_window > 0 else 200000
|
|
436
|
+
context_mgr = ContextManager(
|
|
437
|
+
max_tokens=context_window,
|
|
438
|
+
preserve_recent=max(10, context_window // 5000) # Preservar más mensajes en contextos grandes
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
executor = ToolExecutor(config.root_dir, perms, task_manager=task_mgr)
|
|
442
|
+
executor.perms = perms
|
|
443
|
+
executor.memory = memory_mgr
|
|
444
|
+
|
|
445
|
+
# ── Callback for ask_user ────────────────────────────────────────────────
|
|
446
|
+
def ask_user_callback(question: str, header: str, options: list, multi_select: bool) -> dict:
|
|
447
|
+
"""Callback for the model to ask the user interactively."""
|
|
448
|
+
return ui.ask_user_question(question, header, options, multi_select)
|
|
449
|
+
executor.ask_user_callback = ask_user_callback
|
|
450
|
+
|
|
451
|
+
session_mgr = SessionManager()
|
|
452
|
+
session_mgr.new_session(str(config.root_dir), config.provider, config.model_id)
|
|
453
|
+
|
|
454
|
+
# ── Plugins: package as base + local on top ───────────────────────────
|
|
455
|
+
pkg_plugins = Path(__file__).parent / "plugins"
|
|
456
|
+
plugins_local = config.root_dir / "plugins"
|
|
457
|
+
plugin_mgr = PluginManager(pkg_plugins) if pkg_plugins.exists() else PluginManager(plugins_local)
|
|
458
|
+
if plugins_local.exists() and plugins_local.is_dir():
|
|
459
|
+
local_mgr = PluginManager(plugins_local)
|
|
460
|
+
for name, plugin in local_mgr.plugins.items():
|
|
461
|
+
plugin_mgr.plugins[name] = plugin
|
|
462
|
+
|
|
463
|
+
connector = _make_connector(config, ui)
|
|
464
|
+
|
|
465
|
+
# ── Progress callbacks for subagents ────────────────────────────────────
|
|
466
|
+
def subagent_progress_callback(agent_id: str, event: str, data: dict):
|
|
467
|
+
"""Display subagent progress in UI."""
|
|
468
|
+
if event == "started":
|
|
469
|
+
ui.info(f"🤖 Subagent [{data.get('type', '?')}] started: {data.get('task', '')[:50]}...")
|
|
470
|
+
elif event == "tool_start":
|
|
471
|
+
ui.info(f" ⚡ {data.get('tool', '?')}...")
|
|
472
|
+
elif event == "tool_end":
|
|
473
|
+
status = "✓" if data.get("success", False) else "✗"
|
|
474
|
+
ui.info(f" {status} {data.get('tool', '?')}")
|
|
475
|
+
elif event == "completed":
|
|
476
|
+
tokens = data.get("tokens", 0)
|
|
477
|
+
ui.info(f"✓ Subagent completed ({tokens:,} tokens)")
|
|
478
|
+
elif event == "error":
|
|
479
|
+
ui.error(f"✗ Subagent error: {data.get('error', 'Unknown')}")
|
|
480
|
+
|
|
481
|
+
# ── Subagent Manager (task delegation) ────────────────────────────────
|
|
482
|
+
subagent_mgr = SubagentManager(
|
|
483
|
+
connector=connector,
|
|
484
|
+
tool_executor=executor,
|
|
485
|
+
session_manager=session_mgr,
|
|
486
|
+
permission_manager=perms,
|
|
487
|
+
root_dir=config.root_dir,
|
|
488
|
+
progress_callback=subagent_progress_callback,
|
|
489
|
+
)
|
|
490
|
+
executor.subagents = subagent_mgr
|
|
491
|
+
|
|
492
|
+
# ── Plan Mode (pre-execution planning) ───────────────────────────────
|
|
493
|
+
plan_mode = PlanMode(config.root_dir)
|
|
494
|
+
|
|
495
|
+
# ── Skill Manager ──────────────────────────────────────────────────────────
|
|
496
|
+
skill_mgr = get_skill_manager()
|
|
497
|
+
|
|
498
|
+
# ── Monitor Manager ─────────────────────────────────────────────────────────
|
|
499
|
+
monitor_mgr = get_monitor_manager()
|
|
500
|
+
executor.plan_mode = plan_mode
|
|
501
|
+
|
|
502
|
+
engine = _make_engine(config, connector, executor, session_mgr, plugin_mgr, ui, context_mgr)
|
|
503
|
+
|
|
504
|
+
# ── System prompt from active profile ──────────────────────────────────
|
|
505
|
+
plugin_docs = plugin_mgr.get_plugin_docs()
|
|
506
|
+
if active_profile:
|
|
507
|
+
system_prompt = active_profile.system_prompt
|
|
508
|
+
if plugin_docs:
|
|
509
|
+
system_prompt += f"\n\n## Available Plugins\n\n{plugin_docs}"
|
|
510
|
+
else:
|
|
511
|
+
system_prompt = config.load_system_prompt(plugin_docs)
|
|
512
|
+
|
|
513
|
+
engine.set_system_prompt(system_prompt)
|
|
514
|
+
|
|
515
|
+
# ── Load persistent memory ─────────────────────────────────────────────
|
|
516
|
+
memory_context = memory_mgr.load_memories()
|
|
517
|
+
if memory_context:
|
|
518
|
+
engine._messages.append({"role": "user", "content": memory_context})
|
|
519
|
+
engine._messages.append({"role": "assistant", "content": "Memory loaded. I understand the previous context."})
|
|
520
|
+
|
|
521
|
+
# ── Banner and status ───────────────────────────────────────────────────────
|
|
522
|
+
ui.banner()
|
|
523
|
+
ml = "native" if connector.NATIVE_TOOLS else "XML"
|
|
524
|
+
profile_name = active_profile.meta.display if active_profile else "no profile"
|
|
525
|
+
ui.info(f"Profile: {profile_name} | {connector.provider_name} | Tools: {ml} | Dir: {cwd}")
|
|
526
|
+
context = _build_context(config)
|
|
527
|
+
engine.inject_context(context)
|
|
528
|
+
ui.show_status(config, plugin_mgr, session_mgr.current)
|
|
529
|
+
ui.show_commands(plugin_mgr, skill_mgr)
|
|
530
|
+
|
|
531
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
532
|
+
# MAIN LOOP
|
|
533
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
534
|
+
_chrome_input = False # Track if input came from Chrome
|
|
535
|
+
_telegram_input = False # Track if input came from Telegram
|
|
536
|
+
_telegram_chat_id = None # Store Telegram chat ID for responses
|
|
537
|
+
import select
|
|
538
|
+
|
|
539
|
+
# Import Chrome plugin ONCE at the start
|
|
540
|
+
try:
|
|
541
|
+
from hanus.plugins import chrome as chrome_plugin
|
|
542
|
+
except ImportError:
|
|
543
|
+
chrome_plugin = None
|
|
544
|
+
|
|
545
|
+
# Import Telegram plugin ONCE at the start
|
|
546
|
+
try:
|
|
547
|
+
from hanus.plugins import telegram as telegram_plugin
|
|
548
|
+
except ImportError:
|
|
549
|
+
telegram_plugin = None
|
|
550
|
+
|
|
551
|
+
# Register Telegram callbacks to access agent state
|
|
552
|
+
if telegram_plugin:
|
|
553
|
+
def _telegram_status_callback():
|
|
554
|
+
"""Get current agent status."""
|
|
555
|
+
return {
|
|
556
|
+
"running": True,
|
|
557
|
+
"model": f"{config.provider}/{config.model_id}" if config else "unknown",
|
|
558
|
+
"project": str(config.root_dir) if config else "none",
|
|
559
|
+
"session": session_mgr.current if session_mgr else None,
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
def _telegram_project_callback(path=None):
|
|
563
|
+
"""Get or set current project."""
|
|
564
|
+
if path:
|
|
565
|
+
config.root_dir = Path(path)
|
|
566
|
+
config.save()
|
|
567
|
+
return {"path": str(config.root_dir)}
|
|
568
|
+
return {"path": str(config.root_dir) if config else "none"}
|
|
569
|
+
|
|
570
|
+
def _telegram_model_callback(name=None):
|
|
571
|
+
"""Get or set current model."""
|
|
572
|
+
if name:
|
|
573
|
+
parts = name.split("/")
|
|
574
|
+
if len(parts) == 2:
|
|
575
|
+
config.provider = parts[0]
|
|
576
|
+
config.model_id = parts[1]
|
|
577
|
+
config.save()
|
|
578
|
+
return f"{config.provider}/{config.model_id}"
|
|
579
|
+
return f"{config.provider}/{config.model_id}" if config else "unknown"
|
|
580
|
+
|
|
581
|
+
def _telegram_stop_callback():
|
|
582
|
+
"""Stop current execution."""
|
|
583
|
+
# Signal stop via interrupt
|
|
584
|
+
import signal
|
|
585
|
+
import threading
|
|
586
|
+
import time
|
|
587
|
+
# This will be handled by the main loop
|
|
588
|
+
pass
|
|
589
|
+
|
|
590
|
+
def _telegram_history_callback(n=10):
|
|
591
|
+
"""Get recent message history."""
|
|
592
|
+
if not engine:
|
|
593
|
+
return []
|
|
594
|
+
messages = engine.get_messages()[-n:] if engine else []
|
|
595
|
+
return [{"role": m.get("role", ""), "content": m.get("content", "")[:200]} for m in messages]
|
|
596
|
+
|
|
597
|
+
def _telegram_logs_callback(n=20):
|
|
598
|
+
"""Get recent logs."""
|
|
599
|
+
log_file = Path.home() / ".hanus" / "logs" / "hanus.log"
|
|
600
|
+
if not log_file.exists():
|
|
601
|
+
return []
|
|
602
|
+
try:
|
|
603
|
+
lines = log_file.read_text().splitlines()[-n:]
|
|
604
|
+
return lines
|
|
605
|
+
except:
|
|
606
|
+
return []
|
|
607
|
+
|
|
608
|
+
# Register callbacks
|
|
609
|
+
telegram_plugin.register_callbacks(
|
|
610
|
+
status_callback=_telegram_status_callback,
|
|
611
|
+
project_callback=_telegram_project_callback,
|
|
612
|
+
model_callback=_telegram_model_callback,
|
|
613
|
+
stop_callback=_telegram_stop_callback,
|
|
614
|
+
history_callback=_telegram_history_callback,
|
|
615
|
+
logs_callback=_telegram_logs_callback,
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
def _get_input_with_chrome_poll(timeout=0.5):
|
|
619
|
+
"""Get input from user, Chrome, or Telegram, with polling."""
|
|
620
|
+
while True:
|
|
621
|
+
# Check Chrome messages first
|
|
622
|
+
if chrome_plugin:
|
|
623
|
+
try:
|
|
624
|
+
pending = chrome_plugin.get_pending_chat_messages()
|
|
625
|
+
if pending:
|
|
626
|
+
return ("chrome", pending)
|
|
627
|
+
except Exception as e:
|
|
628
|
+
pass
|
|
629
|
+
|
|
630
|
+
# Check Telegram messages
|
|
631
|
+
if telegram_plugin:
|
|
632
|
+
try:
|
|
633
|
+
if telegram_plugin.has_pending_messages():
|
|
634
|
+
pending = telegram_plugin.get_pending_messages()
|
|
635
|
+
if pending:
|
|
636
|
+
return ("telegram", pending)
|
|
637
|
+
except Exception as e:
|
|
638
|
+
pass
|
|
639
|
+
|
|
640
|
+
# Check if user input is available
|
|
641
|
+
if select.select([sys.stdin], [], [], timeout)[0]:
|
|
642
|
+
line = sys.stdin.readline().strip()
|
|
643
|
+
return ("user", line)
|
|
644
|
+
|
|
645
|
+
while True:
|
|
646
|
+
try:
|
|
647
|
+
_chrome_input = False
|
|
648
|
+
_telegram_input = False
|
|
649
|
+
_telegram_chat_id = None
|
|
650
|
+
user_input = None
|
|
651
|
+
|
|
652
|
+
# Get input from Chrome, Telegram, or user
|
|
653
|
+
source, data = _get_input_with_chrome_poll()
|
|
654
|
+
|
|
655
|
+
if source == "chrome":
|
|
656
|
+
# Process Chrome message(s)
|
|
657
|
+
user_input = data[-1]['message']
|
|
658
|
+
_chrome_input = True
|
|
659
|
+
for msg in data:
|
|
660
|
+
print(f"\n{P.CYAN}📱 [Chrome] Mensaje:{P.R} {msg['message']}")
|
|
661
|
+
print(f"{P.CYAN}▶ Procesando mensaje de Chrome...{P.R}\n")
|
|
662
|
+
elif source == "telegram":
|
|
663
|
+
# Process Telegram message(s)
|
|
664
|
+
user_input = data[-1]['message']
|
|
665
|
+
_telegram_chat_id = data[-1].get('chat_id')
|
|
666
|
+
_telegram_input = True
|
|
667
|
+
for msg in data:
|
|
668
|
+
username = msg.get('username', 'User')
|
|
669
|
+
print(f"\n{P.CYAN}📱 [Telegram] @{username}:{P.R} {msg['message']}")
|
|
670
|
+
print(f"{P.CYAN}▶ Processing Telegram message...{P.R}\n")
|
|
671
|
+
else:
|
|
672
|
+
# User input
|
|
673
|
+
user_input = data
|
|
674
|
+
|
|
675
|
+
if not user_input:
|
|
676
|
+
continue
|
|
677
|
+
|
|
678
|
+
log.info(f"USER: {user_input[:120]}")
|
|
679
|
+
|
|
680
|
+
# ── Exit ─────────────────────────────────────────────────────────
|
|
681
|
+
# Normalize input to detect exit commands
|
|
682
|
+
normalized = user_input.lower().strip()
|
|
683
|
+
# Also detect with extra spaces or control characters
|
|
684
|
+
normalized = ''.join(c for c in normalized if c.isalnum() or c.isspace()).strip()
|
|
685
|
+
if normalized in ("salir", "q", "exit", "quit"):
|
|
686
|
+
_autosave(config, session_mgr, engine, ui)
|
|
687
|
+
ui.goodbye()
|
|
688
|
+
break
|
|
689
|
+
|
|
690
|
+
# ── /profile — profile management ────────────────────────────────
|
|
691
|
+
if _cmd_is(user_input, "/profile"):
|
|
692
|
+
parts = user_input.strip().split(maxsplit=2)
|
|
693
|
+
sub = parts[1].lower() if len(parts) > 1 else ""
|
|
694
|
+
arg = parts[2].strip() if len(parts) > 2 else ""
|
|
695
|
+
|
|
696
|
+
if not sub or sub == "info":
|
|
697
|
+
# /profile → show active profile
|
|
698
|
+
ui.show_profile(active_profile)
|
|
699
|
+
continue
|
|
700
|
+
|
|
701
|
+
if sub == "list":
|
|
702
|
+
# /profile list → list all
|
|
703
|
+
ui.show_profile(active_profile, all_profiles=profile_mgr.list_profiles())
|
|
704
|
+
continue
|
|
705
|
+
|
|
706
|
+
if sub == "new":
|
|
707
|
+
# /profile new <name> [description]
|
|
708
|
+
if not arg:
|
|
709
|
+
ui.error("Usage: /profile new <name>")
|
|
710
|
+
continue
|
|
711
|
+
name_parts = arg.split(maxsplit=1)
|
|
712
|
+
pname = name_parts[0].lower().replace(" ", "_")
|
|
713
|
+
desc = name_parts[1] if len(name_parts) > 1 else f"Profile {pname}"
|
|
714
|
+
base_input = input(f" Based on profile (developer/security/empty): ").strip()
|
|
715
|
+
new_p = profile_mgr.create(pname, pname.title(), desc,
|
|
716
|
+
base=base_input or None)
|
|
717
|
+
ui.success(f"Profile '{pname}' created.")
|
|
718
|
+
prompt_path = profile_mgr.edit_prompt_path(pname)
|
|
719
|
+
ui.info(f"Edit prompt at: {prompt_path}")
|
|
720
|
+
continue
|
|
721
|
+
|
|
722
|
+
if sub == "edit":
|
|
723
|
+
# /profile edit <name>
|
|
724
|
+
pname = arg or (active_profile.name if active_profile else "")
|
|
725
|
+
prompt_path = profile_mgr.edit_prompt_path(pname)
|
|
726
|
+
if prompt_path:
|
|
727
|
+
ui.info(f"Edit prompt at:\n {prompt_path}")
|
|
728
|
+
# Try to open with system editor
|
|
729
|
+
import os
|
|
730
|
+
editor = os.environ.get("EDITOR", "")
|
|
731
|
+
if editor:
|
|
732
|
+
import subprocess
|
|
733
|
+
subprocess.Popen([editor, str(prompt_path)])
|
|
734
|
+
ui.info(f"Opening with {editor}...")
|
|
735
|
+
else:
|
|
736
|
+
ui.error(f"Profile '{pname}' not found.")
|
|
737
|
+
continue
|
|
738
|
+
|
|
739
|
+
if sub == "reload":
|
|
740
|
+
# /profile reload → reload active profile prompt
|
|
741
|
+
if active_profile:
|
|
742
|
+
active_profile = profile_mgr.get(active_profile.name)
|
|
743
|
+
if active_profile:
|
|
744
|
+
system_prompt = active_profile.system_prompt
|
|
745
|
+
if plugin_docs:
|
|
746
|
+
system_prompt += f"\n\n## Available Plugins\n\n{plugin_docs}"
|
|
747
|
+
engine.set_system_prompt(system_prompt)
|
|
748
|
+
ui.success(f"Profile '{active_profile.name}' reloaded.")
|
|
749
|
+
continue
|
|
750
|
+
|
|
751
|
+
if sub == "reinstall":
|
|
752
|
+
# /profile reinstall → reinstall builtin profiles
|
|
753
|
+
reinstalled = profile_mgr.reinstall_builtins()
|
|
754
|
+
if reinstalled:
|
|
755
|
+
ui.success(f"Profiles reinstalled: {', '.join(reinstalled)}")
|
|
756
|
+
else:
|
|
757
|
+
ui.info("No profiles were reinstalled.")
|
|
758
|
+
continue
|
|
759
|
+
|
|
760
|
+
# /profile <name> → switch profile
|
|
761
|
+
pname = sub # sub is the profile name
|
|
762
|
+
new_p = profile_mgr.get(pname)
|
|
763
|
+
if not new_p:
|
|
764
|
+
available = [m.name for m in profile_mgr.list_profiles()]
|
|
765
|
+
ui.error(f"Profile '{pname}' not found. Available: {available}")
|
|
766
|
+
ui.info("Create one with: /profile new <name>")
|
|
767
|
+
continue
|
|
768
|
+
|
|
769
|
+
# Switch active profile
|
|
770
|
+
profile_mgr.set_active(pname)
|
|
771
|
+
active_profile = new_p
|
|
772
|
+
profile_mgr.apply_to_config(active_profile, config)
|
|
773
|
+
|
|
774
|
+
# Rebuild connector if provider changed
|
|
775
|
+
connector = _make_connector(config, ui)
|
|
776
|
+
engine.connector = connector
|
|
777
|
+
|
|
778
|
+
# Rebuild system prompt with new profile
|
|
779
|
+
system_prompt = active_profile.system_prompt
|
|
780
|
+
if plugin_docs:
|
|
781
|
+
system_prompt += f"\n\n## Available Plugins\n\n{plugin_docs}"
|
|
782
|
+
engine.set_system_prompt(system_prompt)
|
|
783
|
+
|
|
784
|
+
# Adjust permissions according to profile
|
|
785
|
+
if active_profile.meta.permission_mode:
|
|
786
|
+
try:
|
|
787
|
+
nm = PermissionMode(active_profile.meta.permission_mode)
|
|
788
|
+
perms.set_mode(nm)
|
|
789
|
+
except ValueError:
|
|
790
|
+
pass
|
|
791
|
+
|
|
792
|
+
ui.success(f"Profile changed to: {active_profile.meta.display}")
|
|
793
|
+
ui.show_profile(active_profile)
|
|
794
|
+
log.info(f"Profile changed to: {pname}")
|
|
795
|
+
|
|
796
|
+
# Ask if new session
|
|
797
|
+
try:
|
|
798
|
+
ans = input(" Start fresh session with new profile? [Y/n] ").strip().lower()
|
|
799
|
+
except (EOFError, KeyboardInterrupt):
|
|
800
|
+
ans = "y"
|
|
801
|
+
if not ans or ans.startswith("y"):
|
|
802
|
+
engine.clear(system_prompt, _build_context(config))
|
|
803
|
+
session_mgr.new_session(str(config.root_dir), config.provider, config.model_id)
|
|
804
|
+
ui.success("New session started with active profile.")
|
|
805
|
+
continue
|
|
806
|
+
|
|
807
|
+
# ── Rest of commands ──────────────────────────────────────────────
|
|
808
|
+
if _cmd_exact(user_input, "/reload"):
|
|
809
|
+
ui.info("Reloading context...")
|
|
810
|
+
ctx = _build_context(config)
|
|
811
|
+
engine._messages.append({"role": "user", "content": f"[Context updated]\n\n{ctx}"})
|
|
812
|
+
ui.success("Context reloaded.")
|
|
813
|
+
continue
|
|
814
|
+
|
|
815
|
+
if _cmd_exact(user_input, "/stream"):
|
|
816
|
+
enabled = ui.toggle_streaming()
|
|
817
|
+
status = "enabled" if enabled else "disabled"
|
|
818
|
+
ui.success(f"Streaming {status}")
|
|
819
|
+
continue
|
|
820
|
+
|
|
821
|
+
if _cmd_exact(user_input, "/multiline"):
|
|
822
|
+
# Mode for pasting multiple lines
|
|
823
|
+
multiline_input = ui.prompt_multiline("Paste your code/text (empty line to finish)")
|
|
824
|
+
if multiline_input:
|
|
825
|
+
user_input = multiline_input
|
|
826
|
+
# Don't continue, process input normally
|
|
827
|
+
else:
|
|
828
|
+
continue
|
|
829
|
+
|
|
830
|
+
# ── /ask — simple chat without tools ────────────────────────────────────
|
|
831
|
+
if _cmd_is(user_input, "/ask"):
|
|
832
|
+
parts = user_input.split(maxsplit=1)
|
|
833
|
+
if len(parts) < 2:
|
|
834
|
+
ui.info("Usage: /ask <question>")
|
|
835
|
+
ui.info("Send a simple question without executing any tools.")
|
|
836
|
+
continue
|
|
837
|
+
|
|
838
|
+
question = parts[1].strip()
|
|
839
|
+
engine.set_chat_mode(True) # Enable chat mode (no tools)
|
|
840
|
+
try:
|
|
841
|
+
response = engine.send(question)
|
|
842
|
+
if response.text:
|
|
843
|
+
_show_response(response, ui)
|
|
844
|
+
finally:
|
|
845
|
+
engine.set_chat_mode(False) # Disable chat mode
|
|
846
|
+
continue
|
|
847
|
+
|
|
848
|
+
if _cmd_exact(user_input, "/files"):
|
|
849
|
+
ui.show_files(list_files(str(config.root_dir))); continue
|
|
850
|
+
|
|
851
|
+
if _cmd_exact(user_input, "/status"):
|
|
852
|
+
ui.show_status(config, plugin_mgr, session_mgr.current); continue
|
|
853
|
+
|
|
854
|
+
if _cmd_exact(user_input, "/plugins"):
|
|
855
|
+
ui.show_plugins(plugin_mgr); continue
|
|
856
|
+
|
|
857
|
+
if _cmd_exact(user_input, "/history"):
|
|
858
|
+
ui.show_history(engine.get_messages()); continue
|
|
859
|
+
|
|
860
|
+
if _cmd_exact(user_input, "/clear"):
|
|
861
|
+
engine.clear(system_prompt, _build_context(config))
|
|
862
|
+
ui.success("Conversation cleared."); continue
|
|
863
|
+
|
|
864
|
+
if _cmd_exact(user_input, "/sessions"):
|
|
865
|
+
ui.show_sessions(session_mgr.list_sessions()); continue
|
|
866
|
+
|
|
867
|
+
if _cmd_is(user_input, "/resume"):
|
|
868
|
+
parts = user_input.split(maxsplit=1)
|
|
869
|
+
sid = parts[1].strip() if len(parts) > 1 else None
|
|
870
|
+
sd = session_mgr.load_by_id(sid) if sid else session_mgr.load_latest(str(config.root_dir))
|
|
871
|
+
if sd:
|
|
872
|
+
session_mgr.current = sd
|
|
873
|
+
engine.set_messages(sd.messages)
|
|
874
|
+
ui.success(f"Session resumed: {sd.name}")
|
|
875
|
+
else:
|
|
876
|
+
ui.error("Session not found.")
|
|
877
|
+
continue
|
|
878
|
+
|
|
879
|
+
if _cmd_exact(user_input, "/undo"):
|
|
880
|
+
rev = session_mgr.undo_last()
|
|
881
|
+
ui.success(f"Reverted: {rev}") if rev else ui.warning("No changes to undo.")
|
|
882
|
+
continue
|
|
883
|
+
|
|
884
|
+
if _cmd_exact(user_input, "/new"):
|
|
885
|
+
engine.clear(system_prompt, _build_context(config))
|
|
886
|
+
session_mgr.new_session(str(config.root_dir), config.provider, config.model_id)
|
|
887
|
+
ui.success("New session started.")
|
|
888
|
+
log.info("New session started")
|
|
889
|
+
continue
|
|
890
|
+
|
|
891
|
+
if _cmd_is(user_input, "/mode"):
|
|
892
|
+
parts = user_input.strip().split(maxsplit=1)
|
|
893
|
+
if len(parts) > 1:
|
|
894
|
+
try:
|
|
895
|
+
nm = PermissionMode(parts[1].strip().lower())
|
|
896
|
+
perms.set_mode(nm); config.permission_mode = nm.value
|
|
897
|
+
ui.success(f"Mode: {nm.value.upper()}")
|
|
898
|
+
except ValueError:
|
|
899
|
+
ui.error(f"Options: {[m.value for m in PermissionMode]}")
|
|
900
|
+
else:
|
|
901
|
+
ui.info(f"Current mode: {perms.mode.value}")
|
|
902
|
+
continue
|
|
903
|
+
|
|
904
|
+
if _cmd_is(user_input, "/plugins"):
|
|
905
|
+
from hanus.plugin_manager import run_plugin_command
|
|
906
|
+
parts = user_input.strip().split(maxsplit=1)
|
|
907
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
908
|
+
result = run_plugin_command(args, plugin_mgr)
|
|
909
|
+
print(result)
|
|
910
|
+
# Refresh system prompt if plugins changed
|
|
911
|
+
if any(x in args.lower() for x in ["enable", "disable", "toggle", "reload"]):
|
|
912
|
+
plugin_docs = plugin_mgr.get_plugin_docs()
|
|
913
|
+
system_prompt = config.load_system_prompt(plugin_docs)
|
|
914
|
+
engine.set_system_prompt(system_prompt)
|
|
915
|
+
ui.info("System prompt updated.")
|
|
916
|
+
continue
|
|
917
|
+
|
|
918
|
+
if _cmd_exact(user_input, "/models"):
|
|
919
|
+
ui.show_models(connector); continue
|
|
920
|
+
|
|
921
|
+
if _cmd_is(user_input, "/model"):
|
|
922
|
+
parts = user_input.strip().split(maxsplit=2)
|
|
923
|
+
if len(parts) == 1:
|
|
924
|
+
ui.info(f"Current: {config.provider}/{config.model_id}")
|
|
925
|
+
ui.info(f"Providers: {ConnectorRegistry.available()}")
|
|
926
|
+
elif len(parts) == 2:
|
|
927
|
+
arg = parts[1].strip()
|
|
928
|
+
if arg.lower() in ConnectorRegistry.available():
|
|
929
|
+
config.provider = arg.lower()
|
|
930
|
+
else:
|
|
931
|
+
config.model_id = arg
|
|
932
|
+
connector = _make_connector(config, ui)
|
|
933
|
+
engine.connector = connector
|
|
934
|
+
engine.set_system_prompt(system_prompt)
|
|
935
|
+
ml = "native" if connector.NATIVE_TOOLS else "XML"
|
|
936
|
+
ui.success(f"Model: {config.provider}/{config.model_id} ({ml})")
|
|
937
|
+
else:
|
|
938
|
+
config.provider = parts[1].strip().lower()
|
|
939
|
+
config.model_id = parts[2].strip()
|
|
940
|
+
connector = _make_connector(config, ui)
|
|
941
|
+
engine.connector = connector
|
|
942
|
+
engine.set_system_prompt(system_prompt)
|
|
943
|
+
ml = "native" if connector.NATIVE_TOOLS else "XML"
|
|
944
|
+
ui.success(f"Model: {config.provider}/{config.model_id} ({ml})")
|
|
945
|
+
continue
|
|
946
|
+
|
|
947
|
+
if _cmd_is(user_input, "/budget"):
|
|
948
|
+
parts = user_input.strip().split(maxsplit=1)
|
|
949
|
+
if len(parts) > 1:
|
|
950
|
+
try:
|
|
951
|
+
config.budget_usd = float(parts[1]); engine.budget_usd = config.budget_usd
|
|
952
|
+
ui.success(f"Budget: ${config.budget_usd:.2f}")
|
|
953
|
+
except ValueError:
|
|
954
|
+
ui.error("Usage: /budget 20.0")
|
|
955
|
+
else:
|
|
956
|
+
spent = session_mgr.current.total_cost_usd if session_mgr.current else 0
|
|
957
|
+
lim = f"/ ${config.budget_usd:.2f}" if config.budget_usd > 0 else "(no limit)"
|
|
958
|
+
ui.info(f"Spent: ${spent:.4f} {lim}")
|
|
959
|
+
continue
|
|
960
|
+
|
|
961
|
+
if _cmd_is(user_input, "/config"):
|
|
962
|
+
parts = user_input.strip().split(maxsplit=2)
|
|
963
|
+
subcmd = parts[1].lower() if len(parts) > 1 else "show"
|
|
964
|
+
|
|
965
|
+
if subcmd == "show" or subcmd == "list":
|
|
966
|
+
# Show current configuration
|
|
967
|
+
print(f"\n{P.HEADER}── CONFIGURATION ─{P.R}")
|
|
968
|
+
print(f" Provider: {config.provider}")
|
|
969
|
+
print(f" Model: {config.model_id}")
|
|
970
|
+
print(f" Max Tokens: {config.max_tokens}")
|
|
971
|
+
print(f" Context: {config.context_window:,} tokens")
|
|
972
|
+
print(f" Permission: {config.permission_mode}")
|
|
973
|
+
print(f" Budget: ${config.budget_usd:.2f}")
|
|
974
|
+
print(f" Root Dir: {config.root_dir}")
|
|
975
|
+
print(f" Ollama URL: {config.ollama_url}")
|
|
976
|
+
print(f" Auto-save: {config.auto_save_session}")
|
|
977
|
+
print(f" Verbose: {config.verbose}")
|
|
978
|
+
print(f" Show Cost: {config.show_cost}")
|
|
979
|
+
print()
|
|
980
|
+
|
|
981
|
+
elif subcmd == "export":
|
|
982
|
+
# Export configuration to global config file
|
|
983
|
+
from hanus.config import CONFIG_DIR, GLOBAL_CONFIG
|
|
984
|
+
try:
|
|
985
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
986
|
+
config_yaml = f"""# ═══════════════════════════════════════════════════════════════════════════════
|
|
987
|
+
# HANUSCODE CONFIGURATION
|
|
988
|
+
# Generated: {datetime.now().isoformat()}
|
|
989
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
990
|
+
|
|
991
|
+
# AI Model
|
|
992
|
+
provider: {config.provider}
|
|
993
|
+
model_id: {config.model_id}
|
|
994
|
+
max_tokens: {config.max_tokens}
|
|
995
|
+
context_window: {config.context_window}
|
|
996
|
+
|
|
997
|
+
# API Keys (set via environment variables for security)
|
|
998
|
+
# anthropic_api_key: ""
|
|
999
|
+
# openai_api_key: ""
|
|
1000
|
+
# gemini_api_key: ""
|
|
1001
|
+
# glm_api_key: ""
|
|
1002
|
+
ollama_url: "{config.ollama_url}"
|
|
1003
|
+
|
|
1004
|
+
# Permissions
|
|
1005
|
+
permission_mode: {config.permission_mode}
|
|
1006
|
+
max_tool_calls: {config.max_tool_calls}
|
|
1007
|
+
|
|
1008
|
+
# Context
|
|
1009
|
+
context_max_files: {config.context_max_files}
|
|
1010
|
+
context_include_content: {str(config.context_include_content).lower()}
|
|
1011
|
+
context_preview_chars: {config.context_preview_chars}
|
|
1012
|
+
max_file_read_chars: {config.max_file_read_chars}
|
|
1013
|
+
context_compress_threshold: {config.context_compress_threshold}
|
|
1014
|
+
|
|
1015
|
+
# Execution
|
|
1016
|
+
shell_timeout: {config.shell_timeout}
|
|
1017
|
+
python_exec_timeout: {config.python_exec_timeout}
|
|
1018
|
+
|
|
1019
|
+
# Budget
|
|
1020
|
+
budget_usd: {config.budget_usd}
|
|
1021
|
+
budget_warn_pct: {config.budget_warn_pct}
|
|
1022
|
+
|
|
1023
|
+
# Sessions
|
|
1024
|
+
auto_save_session: {str(config.auto_save_session).lower()}
|
|
1025
|
+
|
|
1026
|
+
# UI
|
|
1027
|
+
show_cost: {str(config.show_cost).lower()}
|
|
1028
|
+
verbose: {str(config.verbose).lower()}
|
|
1029
|
+
"""
|
|
1030
|
+
GLOBAL_CONFIG.write_text(config_yaml, encoding="utf-8")
|
|
1031
|
+
ui.success(f"Configuration exported to: {GLOBAL_CONFIG}")
|
|
1032
|
+
except Exception as e:
|
|
1033
|
+
ui.error(f"Failed to export config: {e}")
|
|
1034
|
+
|
|
1035
|
+
elif subcmd == "set":
|
|
1036
|
+
if len(parts) < 3:
|
|
1037
|
+
ui.info("Usage: /config set <key> <value>")
|
|
1038
|
+
ui.info("Keys: provider, model_id, permission_mode, budget_usd, max_tokens, ollama_url")
|
|
1039
|
+
continue
|
|
1040
|
+
key_val = parts[2].split(maxsplit=1)
|
|
1041
|
+
if len(key_val) < 2:
|
|
1042
|
+
ui.error("Usage: /config set <key> <value>")
|
|
1043
|
+
continue
|
|
1044
|
+
key, value = key_val[0].lower(), key_val[1]
|
|
1045
|
+
# Apply config change
|
|
1046
|
+
if hasattr(config, key):
|
|
1047
|
+
try:
|
|
1048
|
+
cur = getattr(config, key)
|
|
1049
|
+
if isinstance(cur, bool):
|
|
1050
|
+
setattr(config, key, value.lower() in ("true", "1", "yes"))
|
|
1051
|
+
elif isinstance(cur, int):
|
|
1052
|
+
setattr(config, key, int(value))
|
|
1053
|
+
elif isinstance(cur, float):
|
|
1054
|
+
setattr(config, key, float(value))
|
|
1055
|
+
else:
|
|
1056
|
+
setattr(config, key, value)
|
|
1057
|
+
ui.success(f"Set {key} = {getattr(config, key)}")
|
|
1058
|
+
# Special handling for provider/model
|
|
1059
|
+
if key == "provider":
|
|
1060
|
+
connector = _make_connector(config, ui)
|
|
1061
|
+
engine.connector = connector
|
|
1062
|
+
except Exception as e:
|
|
1063
|
+
ui.error(f"Failed to set {key}: {e}")
|
|
1064
|
+
else:
|
|
1065
|
+
ui.error(f"Unknown config key: {key}")
|
|
1066
|
+
|
|
1067
|
+
elif subcmd == "reset":
|
|
1068
|
+
# Reset to defaults
|
|
1069
|
+
config.permission_mode = "bypass"
|
|
1070
|
+
config.budget_usd = 0.0
|
|
1071
|
+
config.max_tokens = 8096
|
|
1072
|
+
config.context_window = 200000
|
|
1073
|
+
config.show_cost = True
|
|
1074
|
+
config.verbose = False
|
|
1075
|
+
ui.success("Configuration reset to defaults")
|
|
1076
|
+
|
|
1077
|
+
elif subcmd == "path":
|
|
1078
|
+
from hanus.config import CONFIG_DIR, GLOBAL_CONFIG
|
|
1079
|
+
print(f"\n{P.HEADER}── CONFIG PATHS ─{P.R}")
|
|
1080
|
+
print(f" Global config: {GLOBAL_CONFIG}")
|
|
1081
|
+
print(f" Config dir: {CONFIG_DIR}")
|
|
1082
|
+
print(f" Exists: {GLOBAL_CONFIG.exists()}")
|
|
1083
|
+
print()
|
|
1084
|
+
|
|
1085
|
+
else:
|
|
1086
|
+
ui.info("Usage: /config [show|export|set|reset|path]")
|
|
1087
|
+
continue
|
|
1088
|
+
|
|
1089
|
+
if _cmd_exact(user_input, "/audit"):
|
|
1090
|
+
ui.show_audit(perms.get_audit()); continue
|
|
1091
|
+
|
|
1092
|
+
if _cmd_exact(user_input, "/stats"):
|
|
1093
|
+
ui.show_stats(session_mgr.current); continue
|
|
1094
|
+
|
|
1095
|
+
if _cmd_exact(user_input, "/save"):
|
|
1096
|
+
_autosave(config, session_mgr, engine, ui, force=True); continue
|
|
1097
|
+
|
|
1098
|
+
if _cmd_is(user_input, "/tasks"):
|
|
1099
|
+
parts = user_input.strip().split(maxsplit=1)
|
|
1100
|
+
subcmd = parts[1].lower() if len(parts) > 1 else "list"
|
|
1101
|
+
|
|
1102
|
+
if subcmd == "list" or subcmd == "status":
|
|
1103
|
+
tasks = task_mgr.list_tasks()
|
|
1104
|
+
if not tasks:
|
|
1105
|
+
ui.info("No pending tasks.")
|
|
1106
|
+
else:
|
|
1107
|
+
print(f"\n{P.HEADER}── TASKS ─{P.R}")
|
|
1108
|
+
for t in tasks:
|
|
1109
|
+
status_icon = {"pending": "⏳", "in_progress": "🔄", "completed": "✅", "failed": "❌"}.get(t.status.value, "•")
|
|
1110
|
+
print(f" {status_icon} [{t.id}] {t.subject}")
|
|
1111
|
+
if t.description:
|
|
1112
|
+
print(f" {P.MUTED}{t.description[:60]}{'...' if len(t.description) > 60 else ''}{P.R}")
|
|
1113
|
+
print()
|
|
1114
|
+
elif subcmd == "clear":
|
|
1115
|
+
for t in task_mgr.list_tasks():
|
|
1116
|
+
if t.status.value in ("completed", "failed"):
|
|
1117
|
+
task_mgr.delete_task(t.id)
|
|
1118
|
+
ui.success("Completed tasks cleared.")
|
|
1119
|
+
else:
|
|
1120
|
+
ui.info("Commands: /tasks list | /tasks clear")
|
|
1121
|
+
continue
|
|
1122
|
+
|
|
1123
|
+
if _cmd_is(user_input, "/logs"):
|
|
1124
|
+
_show_logs(ui); continue
|
|
1125
|
+
|
|
1126
|
+
if _cmd_is(user_input, "/plugin"):
|
|
1127
|
+
parts = user_input.strip().split(maxsplit=2)
|
|
1128
|
+
name = parts[1] if len(parts) > 1 else ""
|
|
1129
|
+
args = parts[2] if len(parts) > 2 else ""
|
|
1130
|
+
result = plugin_mgr.run(name, args)
|
|
1131
|
+
if result:
|
|
1132
|
+
ui.plugin_result(name, result)
|
|
1133
|
+
engine._messages.append({"role": "user", "content": f"[Plugin {name}]\n{result}"})
|
|
1134
|
+
continue
|
|
1135
|
+
|
|
1136
|
+
# ── /skill — gestión de skills ─────────────────────────────────────────
|
|
1137
|
+
if _cmd_is(user_input, "/skill"):
|
|
1138
|
+
parts = user_input.strip().split(maxsplit=2)
|
|
1139
|
+
subcmd = parts[1].lower() if len(parts) > 1 else "list"
|
|
1140
|
+
|
|
1141
|
+
if subcmd == "list":
|
|
1142
|
+
skills = skill_mgr.list_skills()
|
|
1143
|
+
if not skills:
|
|
1144
|
+
ui.info("No skills installed. Install with: /skill install <url>")
|
|
1145
|
+
else:
|
|
1146
|
+
print(f"\n{P.HEADER}── SKILLS ─{P.R}")
|
|
1147
|
+
for s in sorted(skills, key=lambda x: x.name):
|
|
1148
|
+
icon = "📝" if s.skill_type.value == "markdown" else "🐍"
|
|
1149
|
+
builtin = "builtin" if s.is_builtin else "user"
|
|
1150
|
+
print(f" {icon} /{s.name:<15} {s.description[:40]:<40} [{builtin}]")
|
|
1151
|
+
print()
|
|
1152
|
+
|
|
1153
|
+
elif subcmd == "install":
|
|
1154
|
+
if len(parts) < 3:
|
|
1155
|
+
ui.error("Usage: /skill install <url>")
|
|
1156
|
+
ui.info("Supports: .py, .md files, GitHub Gists")
|
|
1157
|
+
else:
|
|
1158
|
+
url = parts[2]
|
|
1159
|
+
if "gist.github.com" in url:
|
|
1160
|
+
result = skill_mgr.install_from_gist(url)
|
|
1161
|
+
else:
|
|
1162
|
+
result = skill_mgr.install(url)
|
|
1163
|
+
ui.info(result)
|
|
1164
|
+
|
|
1165
|
+
elif subcmd == "create":
|
|
1166
|
+
# /skill create <name> <description>
|
|
1167
|
+
if len(parts) < 3:
|
|
1168
|
+
ui.error("Usage: /skill create <name> <description>")
|
|
1169
|
+
else:
|
|
1170
|
+
create_parts = parts[2].split(maxsplit=1)
|
|
1171
|
+
name = create_parts[0]
|
|
1172
|
+
desc = create_parts[1] if len(create_parts) > 1 else name
|
|
1173
|
+
result = skill_mgr.create_markdown(name, desc)
|
|
1174
|
+
ui.info(result)
|
|
1175
|
+
|
|
1176
|
+
elif subcmd == "remove" or subcmd == "delete":
|
|
1177
|
+
if len(parts) < 3:
|
|
1178
|
+
ui.error("Usage: /skill remove <name>")
|
|
1179
|
+
else:
|
|
1180
|
+
result = skill_mgr.remove(parts[2])
|
|
1181
|
+
ui.info(result)
|
|
1182
|
+
|
|
1183
|
+
elif subcmd == "reload":
|
|
1184
|
+
skill_mgr.reload()
|
|
1185
|
+
ui.success(f"Skills reloaded: {len(skill_mgr.list_skills())} available")
|
|
1186
|
+
|
|
1187
|
+
elif subcmd == "info":
|
|
1188
|
+
if len(parts) < 3:
|
|
1189
|
+
ui.error("Usage: /skill info <name>")
|
|
1190
|
+
else:
|
|
1191
|
+
skill = skill_mgr.get(parts[2])
|
|
1192
|
+
if skill:
|
|
1193
|
+
print(skill.get_doc())
|
|
1194
|
+
else:
|
|
1195
|
+
ui.error(f"Skill '{parts[2]}' not found")
|
|
1196
|
+
|
|
1197
|
+
else:
|
|
1198
|
+
# Try to execute skill: /skill <name> [args]
|
|
1199
|
+
skill_name = subcmd
|
|
1200
|
+
skill_args = parts[2] if len(parts) > 2 else ""
|
|
1201
|
+
skill = skill_mgr.get(skill_name)
|
|
1202
|
+
|
|
1203
|
+
if skill:
|
|
1204
|
+
result = skill.run(skill_args)
|
|
1205
|
+
print(result)
|
|
1206
|
+
# For markdown skills, inject into conversation
|
|
1207
|
+
if skill.skill_type.value == "markdown":
|
|
1208
|
+
engine._messages.append({"role": "user", "content": result})
|
|
1209
|
+
else:
|
|
1210
|
+
ui.error(f"Skill '{skill_name}' not found")
|
|
1211
|
+
ui.info("Commands: list, install, create, remove, info, reload")
|
|
1212
|
+
continue
|
|
1213
|
+
|
|
1214
|
+
# ── /monitor — background process monitoring ───────────────────────────
|
|
1215
|
+
if _cmd_is(user_input, "/monitor"):
|
|
1216
|
+
parts = user_input.strip().split(maxsplit=2)
|
|
1217
|
+
subcmd = parts[1].lower() if len(parts) > 1 else "list"
|
|
1218
|
+
|
|
1219
|
+
if subcmd == "list":
|
|
1220
|
+
monitors = monitor_mgr.list()
|
|
1221
|
+
if not monitors:
|
|
1222
|
+
ui.info("No monitors running. Start with: /monitor start <command>")
|
|
1223
|
+
else:
|
|
1224
|
+
print(f"\n{P.HEADER}── MONITORS ─{P.R}")
|
|
1225
|
+
for m in monitors:
|
|
1226
|
+
print(monitor_mgr.format_status(m))
|
|
1227
|
+
print()
|
|
1228
|
+
|
|
1229
|
+
elif subcmd == "start":
|
|
1230
|
+
if len(parts) < 3:
|
|
1231
|
+
ui.error("Usage: /monitor start <command>")
|
|
1232
|
+
ui.info("Example: /monitor start 'npm run build'")
|
|
1233
|
+
else:
|
|
1234
|
+
command = parts[2]
|
|
1235
|
+
name = command.split()[0] if command else "task"
|
|
1236
|
+
|
|
1237
|
+
def on_event(evt):
|
|
1238
|
+
# Callback for events
|
|
1239
|
+
if evt.event_type == "line":
|
|
1240
|
+
print(f" [{name}] {evt.content}")
|
|
1241
|
+
elif evt.event_type == "complete":
|
|
1242
|
+
print(f"\n✓ Monitor completed: {name}")
|
|
1243
|
+
elif evt.event_type == "error":
|
|
1244
|
+
print(f"\n✗ Monitor failed: {name} - {evt.content}")
|
|
1245
|
+
|
|
1246
|
+
monitor_id = monitor_mgr.start(name, command, on_event=on_event, cwd=config.root_dir)
|
|
1247
|
+
ui.success(f"Monitor started: {monitor_id}")
|
|
1248
|
+
ui.info("Check progress with: /monitor list")
|
|
1249
|
+
|
|
1250
|
+
elif subcmd == "stop":
|
|
1251
|
+
if len(parts) < 3:
|
|
1252
|
+
ui.error("Usage: /monitor stop <id>")
|
|
1253
|
+
else:
|
|
1254
|
+
result = monitor_mgr.stop(parts[2])
|
|
1255
|
+
ui.info(result)
|
|
1256
|
+
|
|
1257
|
+
elif subcmd == "events":
|
|
1258
|
+
if len(parts) < 3:
|
|
1259
|
+
ui.error("Usage: /monitor events <id>")
|
|
1260
|
+
else:
|
|
1261
|
+
events = monitor_mgr.get_events(parts[2])
|
|
1262
|
+
for evt in events:
|
|
1263
|
+
print(f" [{evt.event_type}] {evt.content}")
|
|
1264
|
+
|
|
1265
|
+
elif subcmd == "wait":
|
|
1266
|
+
if len(parts) < 3:
|
|
1267
|
+
ui.error("Usage: /monitor wait <id>")
|
|
1268
|
+
else:
|
|
1269
|
+
ui.info(f"Waiting for {parts[2]} to complete...")
|
|
1270
|
+
task = monitor_mgr.wait(parts[2])
|
|
1271
|
+
if task:
|
|
1272
|
+
print(monitor_mgr.format_status(task))
|
|
1273
|
+
|
|
1274
|
+
else:
|
|
1275
|
+
ui.info("Commands: list, start, stop, events, wait")
|
|
1276
|
+
continue
|
|
1277
|
+
|
|
1278
|
+
# ── Invoke skill or plugin directly: /name [args] ──────────────────────
|
|
1279
|
+
if user_input.startswith("/") and len(user_input) > 1:
|
|
1280
|
+
first_space = user_input.find(" ")
|
|
1281
|
+
if first_space > 0:
|
|
1282
|
+
potential_name = user_input[1:first_space].lower()
|
|
1283
|
+
cmd_args = user_input[first_space:].strip()
|
|
1284
|
+
else:
|
|
1285
|
+
potential_name = user_input[1:].lower()
|
|
1286
|
+
cmd_args = ""
|
|
1287
|
+
|
|
1288
|
+
# First check if it's a plugin
|
|
1289
|
+
if potential_name in plugin_mgr.plugins:
|
|
1290
|
+
result = plugin_mgr.run(potential_name, cmd_args)
|
|
1291
|
+
if result:
|
|
1292
|
+
ui.plugin_result(potential_name, result)
|
|
1293
|
+
continue
|
|
1294
|
+
|
|
1295
|
+
# Then check if it's a skill
|
|
1296
|
+
skill = skill_mgr.get(potential_name)
|
|
1297
|
+
if skill:
|
|
1298
|
+
result = skill.run(cmd_args)
|
|
1299
|
+
print(result)
|
|
1300
|
+
# For markdown skills, inject into conversation
|
|
1301
|
+
if skill.skill_type.value == "markdown":
|
|
1302
|
+
engine._messages.append({"role": "user", "content": result})
|
|
1303
|
+
continue
|
|
1304
|
+
|
|
1305
|
+
# ── Agent query ────────────────────────────────────────────
|
|
1306
|
+
try:
|
|
1307
|
+
# Start background input collection (allows typing while agent works)
|
|
1308
|
+
tp = get_prompt()
|
|
1309
|
+
tp.start_background_input()
|
|
1310
|
+
|
|
1311
|
+
response = engine.send(user_input)
|
|
1312
|
+
|
|
1313
|
+
# Stop background input and collect any typed text
|
|
1314
|
+
background_input = tp.stop_background_input()
|
|
1315
|
+
|
|
1316
|
+
# If agent finished but there's pending input, ask user what to do
|
|
1317
|
+
if background_input and response.stop_reason in ("done", "finished", "max_iterations"):
|
|
1318
|
+
ui.info(f"Agent finished, but you typed: '{background_input[:50]}...'")
|
|
1319
|
+
try:
|
|
1320
|
+
choice = input(" Send this as new message? [Y/n]: ").strip().lower()
|
|
1321
|
+
except (EOFError, KeyboardInterrupt):
|
|
1322
|
+
choice = "n"
|
|
1323
|
+
|
|
1324
|
+
if choice in ("", "y", "yes"):
|
|
1325
|
+
# Send as new message
|
|
1326
|
+
engine.set_pending_input(background_input)
|
|
1327
|
+
user_input = background_input
|
|
1328
|
+
# Re-run with the new input
|
|
1329
|
+
response = engine.send(user_input)
|
|
1330
|
+
else:
|
|
1331
|
+
ui.info("Pending input cancelled.")
|
|
1332
|
+
background_input = ""
|
|
1333
|
+
|
|
1334
|
+
elif background_input:
|
|
1335
|
+
# Agent didn't finish, store input for next iteration
|
|
1336
|
+
engine.set_pending_input(background_input)
|
|
1337
|
+
|
|
1338
|
+
except KeyboardInterrupt:
|
|
1339
|
+
engine.interrupt()
|
|
1340
|
+
ui.warning("Model interrupted. You can continue typing.")
|
|
1341
|
+
log.info("Interrupted by Ctrl+C")
|
|
1342
|
+
# Stop background input on interrupt
|
|
1343
|
+
tp = get_prompt()
|
|
1344
|
+
tp.stop_background_input()
|
|
1345
|
+
continue
|
|
1346
|
+
|
|
1347
|
+
if response.stop_reason == "interrupted":
|
|
1348
|
+
ui.warning("Response interrupted.")
|
|
1349
|
+
else:
|
|
1350
|
+
_show_response(response, ui)
|
|
1351
|
+
|
|
1352
|
+
# Send response back to Chrome extension if input came from there
|
|
1353
|
+
if _chrome_input and response.text:
|
|
1354
|
+
try:
|
|
1355
|
+
from hanus.plugins.chrome import send_chat_response
|
|
1356
|
+
# Send response to Chrome
|
|
1357
|
+
response_text = response.text[:4000] # Limit message size
|
|
1358
|
+
if send_chat_response(response_text):
|
|
1359
|
+
print(f"{P.CYAN}📱 [Chrome] Respuesta enviada a la extensión{P.R}")
|
|
1360
|
+
else:
|
|
1361
|
+
print(f"{P.YELLOW}📱 [Chrome] No se pudo enviar la respuesta (extensión no conectada){P.R}")
|
|
1362
|
+
except Exception as e:
|
|
1363
|
+
log.info(f"[Chrome] Response error: {e}")
|
|
1364
|
+
|
|
1365
|
+
# Send response back to Telegram if input came from there
|
|
1366
|
+
if _telegram_input and response.text and _telegram_chat_id:
|
|
1367
|
+
try:
|
|
1368
|
+
from hanus.plugins.telegram import send_response
|
|
1369
|
+
# Send response to Telegram
|
|
1370
|
+
response_text = response.text[:4000] # Limit message size
|
|
1371
|
+
send_response(_telegram_chat_id, response_text)
|
|
1372
|
+
print(f"{P.CYAN}📱 [Telegram] Response sent to chat {P.R}")
|
|
1373
|
+
except Exception as e:
|
|
1374
|
+
log.info(f"[Telegram] Response error: {e}")
|
|
1375
|
+
|
|
1376
|
+
if config.show_cost and session_mgr.current:
|
|
1377
|
+
ui.show_cost_bar(
|
|
1378
|
+
response.cost_usd, response.input_tokens, response.output_tokens,
|
|
1379
|
+
session_mgr.current.total_cost_usd, config.budget_usd,
|
|
1380
|
+
)
|
|
1381
|
+
if config.auto_save_session:
|
|
1382
|
+
_autosave(config, session_mgr, engine, ui)
|
|
1383
|
+
if config.budget_usd > 0 and session_mgr.current:
|
|
1384
|
+
spent = session_mgr.current.total_cost_usd
|
|
1385
|
+
if spent >= config.budget_usd * config.budget_warn_pct:
|
|
1386
|
+
ui.warning(f"Budget at {spent/config.budget_usd*100:.0f}% "
|
|
1387
|
+
f"(${spent:.4f}/${config.budget_usd:.2f})")
|
|
1388
|
+
|
|
1389
|
+
except KeyboardInterrupt:
|
|
1390
|
+
print()
|
|
1391
|
+
ui.warning("Ctrl+C — type 'exit' to quit or Enter to continue.")
|
|
1392
|
+
continue
|
|
1393
|
+
|
|
1394
|
+
except Exception as e:
|
|
1395
|
+
import traceback
|
|
1396
|
+
ui.error(f"{type(e).__name__}: {e}")
|
|
1397
|
+
log.log_error("main loop", e)
|
|
1398
|
+
if config.verbose:
|
|
1399
|
+
print(traceback.format_exc())
|
|
1400
|
+
try:
|
|
1401
|
+
ans = input(" Continue? [Y/n] ").strip().lower()
|
|
1402
|
+
except Exception:
|
|
1403
|
+
ans = "n"
|
|
1404
|
+
if ans.startswith("n"):
|
|
1405
|
+
break
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
def _show_logs(ui):
|
|
1409
|
+
import time as _t
|
|
1410
|
+
log_dir = Path.home() / ".hanus" / "logs"
|
|
1411
|
+
log_file = log_dir / f"hanus_{_t.strftime('%Y%m%d')}.log"
|
|
1412
|
+
if not log_file.exists():
|
|
1413
|
+
ui.info("No logs today.")
|
|
1414
|
+
return
|
|
1415
|
+
lines = log_file.read_text(encoding="utf-8", errors="replace").splitlines()[-40:]
|
|
1416
|
+
print()
|
|
1417
|
+
for line in lines:
|
|
1418
|
+
print(f" {line}")
|
|
1419
|
+
print()
|
|
1420
|
+
|
|
1421
|
+
|
|
1422
|
+
if __name__ == "__main__":
|
|
1423
|
+
# If executed directly, use CLI arguments
|
|
1424
|
+
import argparse
|
|
1425
|
+
parser = argparse.ArgumentParser(
|
|
1426
|
+
prog="hanuscode",
|
|
1427
|
+
description="Autonomous Programming Agent"
|
|
1428
|
+
)
|
|
1429
|
+
parser.add_argument("--cmd", "-c", default=None, help="Command to execute")
|
|
1430
|
+
parser.add_argument("--path", "-p", default=None, help="Working directory")
|
|
1431
|
+
parser.add_argument("--profile", default=None, help="Profile to use")
|
|
1432
|
+
parser.add_argument("--model", default=None, help="Model: provider/model")
|
|
1433
|
+
parser.add_argument("--mode", default=None, choices=["default", "plan", "bypass"])
|
|
1434
|
+
args = parser.parse_args()
|
|
1435
|
+
|
|
1436
|
+
if args.path:
|
|
1437
|
+
import os
|
|
1438
|
+
os.chdir(Path(args.path).expanduser().resolve())
|
|
1439
|
+
|
|
1440
|
+
run_with_args(
|
|
1441
|
+
cmd=args.cmd,
|
|
1442
|
+
profile=args.profile,
|
|
1443
|
+
model=args.model,
|
|
1444
|
+
mode=args.mode
|
|
1445
|
+
)
|