icarus-agent 1.0.0__tar.gz
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.
- icarus_agent-1.0.0/PKG-INFO +84 -0
- icarus_agent-1.0.0/README.md +16 -0
- icarus_agent-1.0.0/cli/__init__.py +1 -0
- icarus_agent-1.0.0/cli/headless.py +106 -0
- icarus_agent-1.0.0/cli/main.py +395 -0
- icarus_agent-1.0.0/cli/skills_commands.py +830 -0
- icarus_agent-1.0.0/cli/ui.py +149 -0
- icarus_agent-1.0.0/core/__init__.py +5 -0
- icarus_agent-1.0.0/core/_version.py +3 -0
- icarus_agent-1.0.0/core/assets/default_agent_prompt.md +12 -0
- icarus_agent-1.0.0/core/assets/memory_instructions.md +27 -0
- icarus_agent-1.0.0/core/assets/pim_instructions.md +33 -0
- icarus_agent-1.0.0/core/assets/system_prompt.md +232 -0
- icarus_agent-1.0.0/core/built_in_skills/__init__.py +5 -0
- icarus_agent-1.0.0/core/built_in_skills/doc-coauthoring/SKILL.md +375 -0
- icarus_agent-1.0.0/core/built_in_skills/docx/SKILL.md +196 -0
- icarus_agent-1.0.0/core/built_in_skills/docx/docx-js.md +350 -0
- icarus_agent-1.0.0/core/built_in_skills/docx/ooxml.md +610 -0
- icarus_agent-1.0.0/core/built_in_skills/frontend-design/SKILL.md +41 -0
- icarus_agent-1.0.0/core/built_in_skills/pdf/SKILL.md +293 -0
- icarus_agent-1.0.0/core/built_in_skills/pdf/forms.md +205 -0
- icarus_agent-1.0.0/core/built_in_skills/pdf/reference.md +612 -0
- icarus_agent-1.0.0/core/built_in_skills/pptx/SKILL.md +483 -0
- icarus_agent-1.0.0/core/built_in_skills/pptx/html2pptx.md +625 -0
- icarus_agent-1.0.0/core/built_in_skills/pptx/ooxml.md +427 -0
- icarus_agent-1.0.0/core/built_in_skills/skill-creator/SKILL.md +399 -0
- icarus_agent-1.0.0/core/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- icarus_agent-1.0.0/core/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- icarus_agent-1.0.0/core/built_in_skills/theme-factory/SKILL.md +39 -0
- icarus_agent-1.0.0/core/built_in_skills/xlsx/SKILL.md +288 -0
- icarus_agent-1.0.0/core/built_in_skills/xlsx/recalc.py +178 -0
- icarus_agent-1.0.0/core/config/__init__.py +1 -0
- icarus_agent-1.0.0/core/config/app_config.py +1345 -0
- icarus_agent-1.0.0/core/config/display.py +248 -0
- icarus_agent-1.0.0/core/config/model_config.py +1212 -0
- icarus_agent-1.0.0/core/config/ollama.py +98 -0
- icarus_agent-1.0.0/core/config/permissions.py +96 -0
- icarus_agent-1.0.0/core/engine/__init__.py +12 -0
- icarus_agent-1.0.0/core/engine/engine.py +528 -0
- icarus_agent-1.0.0/core/integrations/__init__.py +1 -0
- icarus_agent-1.0.0/core/integrations/daytona.py +218 -0
- icarus_agent-1.0.0/core/integrations/langsmith.py +282 -0
- icarus_agent-1.0.0/core/integrations/modal.py +223 -0
- icarus_agent-1.0.0/core/integrations/runloop.py +225 -0
- icarus_agent-1.0.0/core/integrations/sandbox_factory.py +184 -0
- icarus_agent-1.0.0/core/integrations/sandbox_provider.py +71 -0
- icarus_agent-1.0.0/core/mcp/__init__.py +29 -0
- icarus_agent-1.0.0/core/mcp/client.py +375 -0
- icarus_agent-1.0.0/core/mcp/config.py +144 -0
- icarus_agent-1.0.0/core/pim/__init__.py +36 -0
- icarus_agent-1.0.0/core/pim/calendar.py +240 -0
- icarus_agent-1.0.0/core/pim/common.py +168 -0
- icarus_agent-1.0.0/core/pim/contacts.py +248 -0
- icarus_agent-1.0.0/core/pim/todos.py +309 -0
- icarus_agent-1.0.0/core/runtime/__init__.py +9 -0
- icarus_agent-1.0.0/core/runtime/agent.py +673 -0
- icarus_agent-1.0.0/core/runtime/backend.py +137 -0
- icarus_agent-1.0.0/core/runtime/builder.py +123 -0
- icarus_agent-1.0.0/core/runtime/context.py +243 -0
- icarus_agent-1.0.0/core/runtime/local_context.py +517 -0
- icarus_agent-1.0.0/core/runtime/memory_context.py +51 -0
- icarus_agent-1.0.0/core/runtime/pim_context.py +161 -0
- icarus_agent-1.0.0/core/runtime/subagents.py +173 -0
- icarus_agent-1.0.0/core/runtime/time_context.py +120 -0
- icarus_agent-1.0.0/core/session/__init__.py +33 -0
- icarus_agent-1.0.0/core/session/events.py +113 -0
- icarus_agent-1.0.0/core/session/model.py +256 -0
- icarus_agent-1.0.0/core/session/search.py +438 -0
- icarus_agent-1.0.0/core/session/serialize.py +49 -0
- icarus_agent-1.0.0/core/session/stream.py +487 -0
- icarus_agent-1.0.0/core/session/thread_state.py +108 -0
- icarus_agent-1.0.0/core/session/threads.py +436 -0
- icarus_agent-1.0.0/core/session/titles.py +79 -0
- icarus_agent-1.0.0/core/skills/__init__.py +5 -0
- icarus_agent-1.0.0/core/skills/installer.py +238 -0
- icarus_agent-1.0.0/core/skills/load.py +223 -0
- icarus_agent-1.0.0/core/skills/registry.py +133 -0
- icarus_agent-1.0.0/core/skills/registry_config.py +92 -0
- icarus_agent-1.0.0/core/skills/validation.py +113 -0
- icarus_agent-1.0.0/core/tools/__init__.py +251 -0
- icarus_agent-1.0.0/core/tools/browser.py +545 -0
- icarus_agent-1.0.0/core/tools/pim.py +448 -0
- icarus_agent-1.0.0/core/utils/__init__.py +1 -0
- icarus_agent-1.0.0/core/utils/clipboard.py +128 -0
- icarus_agent-1.0.0/core/utils/file_ops.py +472 -0
- icarus_agent-1.0.0/core/utils/image_utils.py +100 -0
- icarus_agent-1.0.0/core/utils/input.py +286 -0
- icarus_agent-1.0.0/core/utils/toml.py +44 -0
- icarus_agent-1.0.0/core/utils/tool_display.py +273 -0
- icarus_agent-1.0.0/desktop/sidecar/__init__.py +1 -0
- icarus_agent-1.0.0/desktop/sidecar/__main__.py +5 -0
- icarus_agent-1.0.0/desktop/sidecar/main.py +1203 -0
- icarus_agent-1.0.0/desktop/sidecar/protocol.py +103 -0
- icarus_agent-1.0.0/icarus_agent.egg-info/PKG-INFO +84 -0
- icarus_agent-1.0.0/icarus_agent.egg-info/SOURCES.txt +170 -0
- icarus_agent-1.0.0/icarus_agent.egg-info/dependency_links.txt +1 -0
- icarus_agent-1.0.0/icarus_agent.egg-info/entry_points.txt +3 -0
- icarus_agent-1.0.0/icarus_agent.egg-info/requires.txt +74 -0
- icarus_agent-1.0.0/icarus_agent.egg-info/top_level.txt +4 -0
- icarus_agent-1.0.0/pyproject.toml +82 -0
- icarus_agent-1.0.0/server/__init__.py +0 -0
- icarus_agent-1.0.0/server/__main__.py +28 -0
- icarus_agent-1.0.0/server/app.py +232 -0
- icarus_agent-1.0.0/server/auth/__init__.py +1 -0
- icarus_agent-1.0.0/server/auth/audit.py +56 -0
- icarus_agent-1.0.0/server/auth/base.py +17 -0
- icarus_agent-1.0.0/server/auth/db.py +138 -0
- icarus_agent-1.0.0/server/auth/local.py +74 -0
- icarus_agent-1.0.0/server/auth/middleware.py +33 -0
- icarus_agent-1.0.0/server/auth/rate_limit.py +73 -0
- icarus_agent-1.0.0/server/auth/routes.py +131 -0
- icarus_agent-1.0.0/server/auth/tokens.py +125 -0
- icarus_agent-1.0.0/server/config.py +82 -0
- icarus_agent-1.0.0/server/engine_pool.py +174 -0
- icarus_agent-1.0.0/server/handler.py +769 -0
- icarus_agent-1.0.0/server/setup_wizard.py +300 -0
- icarus_agent-1.0.0/server/users.py +16 -0
- icarus_agent-1.0.0/server/ws.py +93 -0
- icarus_agent-1.0.0/setup.cfg +48 -0
- icarus_agent-1.0.0/tests/test_agent_e2e.py +89 -0
- icarus_agent-1.0.0/tests/test_builder.py +140 -0
- icarus_agent-1.0.0/tests/test_contacts_migration.py +146 -0
- icarus_agent-1.0.0/tests/test_context.py +105 -0
- icarus_agent-1.0.0/tests/test_mcp_config.py +228 -0
- icarus_agent-1.0.0/tests/test_mcp_resilience.py +472 -0
- icarus_agent-1.0.0/tests/test_mcp_server_death.py +232 -0
- icarus_agent-1.0.0/tests/test_memory_context.py +48 -0
- icarus_agent-1.0.0/tests/test_model_switch.py +432 -0
- icarus_agent-1.0.0/tests/test_models.py +216 -0
- icarus_agent-1.0.0/tests/test_ollama_discovery.py +363 -0
- icarus_agent-1.0.0/tests/test_package_extras.py +65 -0
- icarus_agent-1.0.0/tests/test_permissioned_backend.py +196 -0
- icarus_agent-1.0.0/tests/test_permissions.py +151 -0
- icarus_agent-1.0.0/tests/test_permissions_wiring.py +60 -0
- icarus_agent-1.0.0/tests/test_pim_calendar.py +220 -0
- icarus_agent-1.0.0/tests/test_pim_contacts.py +198 -0
- icarus_agent-1.0.0/tests/test_pim_context.py +157 -0
- icarus_agent-1.0.0/tests/test_pim_todos.py +250 -0
- icarus_agent-1.0.0/tests/test_pim_tools.py +239 -0
- icarus_agent-1.0.0/tests/test_provider_wiring.py +181 -0
- icarus_agent-1.0.0/tests/test_search.py +560 -0
- icarus_agent-1.0.0/tests/test_session_model.py +269 -0
- icarus_agent-1.0.0/tests/test_session_stream.py +573 -0
- icarus_agent-1.0.0/tests/test_sidecar.py +412 -0
- icarus_agent-1.0.0/tests/test_sidecar_concurrent.py +83 -0
- icarus_agent-1.0.0/tests/test_skill_store.py +175 -0
- icarus_agent-1.0.0/tests/test_thread_state.py +213 -0
- icarus_agent-1.0.0/tests/test_threads.py +244 -0
- icarus_agent-1.0.0/tests/test_time_context.py +116 -0
- icarus_agent-1.0.0/tui/__init__.py +1 -0
- icarus_agent-1.0.0/tui/adapter.py +179 -0
- icarus_agent-1.0.0/tui/app.py +2204 -0
- icarus_agent-1.0.0/tui/app.tcss +181 -0
- icarus_agent-1.0.0/tui/widgets/__init__.py +9 -0
- icarus_agent-1.0.0/tui/widgets/_links.py +38 -0
- icarus_agent-1.0.0/tui/widgets/approval.py +334 -0
- icarus_agent-1.0.0/tui/widgets/autocomplete.py +633 -0
- icarus_agent-1.0.0/tui/widgets/chat_input.py +1303 -0
- icarus_agent-1.0.0/tui/widgets/diff.py +215 -0
- icarus_agent-1.0.0/tui/widgets/history.py +160 -0
- icarus_agent-1.0.0/tui/widgets/loading.py +172 -0
- icarus_agent-1.0.0/tui/widgets/message_store.py +611 -0
- icarus_agent-1.0.0/tui/widgets/messages.py +1324 -0
- icarus_agent-1.0.0/tui/widgets/model_selector.py +565 -0
- icarus_agent-1.0.0/tui/widgets/navigable_list.py +101 -0
- icarus_agent-1.0.0/tui/widgets/skill_store.py +983 -0
- icarus_agent-1.0.0/tui/widgets/status.py +279 -0
- icarus_agent-1.0.0/tui/widgets/thread_selector.py +480 -0
- icarus_agent-1.0.0/tui/widgets/tool_renderers.py +128 -0
- icarus_agent-1.0.0/tui/widgets/tool_widgets.py +245 -0
- icarus_agent-1.0.0/tui/widgets/welcome.py +150 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: icarus-agent
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Production-ready AI coding agent built on deepagents SDK
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: <4.0,>=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: deepagents
|
|
9
|
+
Requires-Dist: langchain<2.0.0,>=1.2.10
|
|
10
|
+
Requires-Dist: langchain-openai<2.0.0,>=1.1.8
|
|
11
|
+
Requires-Dist: langgraph-cli[inmem]>=0.1.55
|
|
12
|
+
Requires-Dist: langgraph-checkpoint-sqlite<4.0.0,>=3.0.0
|
|
13
|
+
Requires-Dist: python-dotenv<2.0.0,>=1.0.0
|
|
14
|
+
Requires-Dist: rich>=14.0.0
|
|
15
|
+
Requires-Dist: markdownify>=0.13.0
|
|
16
|
+
Requires-Dist: langsmith>=0.6.6
|
|
17
|
+
Requires-Dist: tavily-python>=0.7.21
|
|
18
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
19
|
+
Requires-Dist: aiosqlite>=0.19.0
|
|
20
|
+
Requires-Dist: tomli-w>=1.0.0
|
|
21
|
+
Requires-Dist: requests>=2.0.0
|
|
22
|
+
Requires-Dist: langchain-mcp-adapters>=0.2.0
|
|
23
|
+
Requires-Dist: pillow>=10.0.0
|
|
24
|
+
Provides-Extra: openai
|
|
25
|
+
Requires-Dist: langchain-openai<2.0.0,>=1.1.8; extra == "openai"
|
|
26
|
+
Provides-Extra: anthropic
|
|
27
|
+
Requires-Dist: langchain-anthropic<2.0.0,>=1.3.3; extra == "anthropic"
|
|
28
|
+
Provides-Extra: azure
|
|
29
|
+
Requires-Dist: langchain-azure-ai<2.0.0,>=1.0.0; extra == "azure"
|
|
30
|
+
Provides-Extra: vertex
|
|
31
|
+
Requires-Dist: langchain-google-vertexai<4.0.0,>=3.0.0; extra == "vertex"
|
|
32
|
+
Provides-Extra: bedrock
|
|
33
|
+
Requires-Dist: langchain-aws<2.0.0,>=1.0.0; extra == "bedrock"
|
|
34
|
+
Provides-Extra: groq
|
|
35
|
+
Requires-Dist: langchain-groq<2.0.0,>=1.0.0; extra == "groq"
|
|
36
|
+
Provides-Extra: ollama
|
|
37
|
+
Requires-Dist: langchain-ollama<2.0.0,>=1.0.0; extra == "ollama"
|
|
38
|
+
Provides-Extra: local
|
|
39
|
+
Requires-Dist: langchain-openai<2.0.0,>=1.1.8; extra == "local"
|
|
40
|
+
Provides-Extra: pim
|
|
41
|
+
Requires-Dist: vobject>=0.9.7; extra == "pim"
|
|
42
|
+
Requires-Dist: icalendar>=6.0.0; extra == "pim"
|
|
43
|
+
Requires-Dist: recurring-ical-events>=3.0.0; extra == "pim"
|
|
44
|
+
Provides-Extra: built-in-skill-libs
|
|
45
|
+
Requires-Dist: pandas>=2.0; extra == "built-in-skill-libs"
|
|
46
|
+
Requires-Dist: openpyxl>=3.1; extra == "built-in-skill-libs"
|
|
47
|
+
Requires-Dist: python-docx>=1.0; extra == "built-in-skill-libs"
|
|
48
|
+
Requires-Dist: fpdf2>=2.7; extra == "built-in-skill-libs"
|
|
49
|
+
Provides-Extra: tui
|
|
50
|
+
Requires-Dist: textual>=0.98.0; extra == "tui"
|
|
51
|
+
Requires-Dist: prompt-toolkit>=3.0.52; extra == "tui"
|
|
52
|
+
Requires-Dist: pyperclip>=1.11.0; extra == "tui"
|
|
53
|
+
Requires-Dist: textual-autocomplete>=3.0.0a12; extra == "tui"
|
|
54
|
+
Provides-Extra: serve
|
|
55
|
+
Requires-Dist: fastapi>=0.115.0; extra == "serve"
|
|
56
|
+
Requires-Dist: uvicorn[standard]>=0.30.0; extra == "serve"
|
|
57
|
+
Requires-Dist: argon2-cffi>=23.1.0; extra == "serve"
|
|
58
|
+
Requires-Dist: PyJWT[crypto]>=2.11.0; extra == "serve"
|
|
59
|
+
Provides-Extra: all
|
|
60
|
+
Requires-Dist: icarus-agent[anthropic,azure,bedrock,groq,ollama,openai,vertex]; extra == "all"
|
|
61
|
+
Requires-Dist: icarus-agent[built-in-skill-libs,pim]; extra == "all"
|
|
62
|
+
Requires-Dist: icarus-agent[serve,tui]; extra == "all"
|
|
63
|
+
Provides-Extra: dev
|
|
64
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
65
|
+
Requires-Dist: pytest-asyncio>=1.0; extra == "dev"
|
|
66
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
67
|
+
Requires-Dist: ruff>=0.12; extra == "dev"
|
|
68
|
+
|
|
69
|
+
# ICARUS
|
|
70
|
+
|
|
71
|
+
Production-ready AI coding agent built on deepagents SDK.
|
|
72
|
+
|
|
73
|
+
## Quick Start
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install -e ".[dev]"
|
|
77
|
+
icarus # Interactive TUI
|
|
78
|
+
icarus run "task" # Headless mode
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
- `~/.icarus/config.toml` — Model providers and defaults
|
|
84
|
+
- `.env` — API keys and secrets
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# ICARUS
|
|
2
|
+
|
|
3
|
+
Production-ready AI coding agent built on deepagents SDK.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e ".[dev]"
|
|
9
|
+
icarus # Interactive TUI
|
|
10
|
+
icarus run "task" # Headless mode
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
- `~/.icarus/config.toml` — Model providers and defaults
|
|
16
|
+
- `.env` — API keys and secrets
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI client for ICARUS — argparse, headless mode, and skill commands."""
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Non-interactive (headless) mode — single message -> stream -> exit.
|
|
2
|
+
|
|
3
|
+
Creates an ICARUS agent and streams the response to stdout.
|
|
4
|
+
Exit codes: 0 success, 1 error, 130 interrupt.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
from icarus.session.events import TextDelta
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def run_headless(
|
|
20
|
+
message: str,
|
|
21
|
+
*,
|
|
22
|
+
model: str | None = None,
|
|
23
|
+
auto_approve: bool = False,
|
|
24
|
+
quiet: bool = False,
|
|
25
|
+
) -> int:
|
|
26
|
+
"""Run a single task non-interactively and exit.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
message: The task/message to execute.
|
|
30
|
+
model: Optional model spec override (e.g. 'azure_ai:Kimi-K2.5').
|
|
31
|
+
auto_approve: Skip HITL approval for all tool calls.
|
|
32
|
+
quiet: Suppress all non-response output.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Exit code: 0 for success, 1 for error, 130 for keyboard interrupt.
|
|
36
|
+
"""
|
|
37
|
+
from icarus.mcp import MCPManager
|
|
38
|
+
from icarus.runtime.builder import create_icarus_agent
|
|
39
|
+
from icarus.runtime.context import UserContext
|
|
40
|
+
from icarus.session.stream import stream_turn
|
|
41
|
+
|
|
42
|
+
console = Console(stderr=True) if quiet else Console()
|
|
43
|
+
llm_model = None
|
|
44
|
+
mcp_manager = None
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
ctx = UserContext.default()
|
|
48
|
+
thread_id = str(uuid4())
|
|
49
|
+
|
|
50
|
+
if not quiet:
|
|
51
|
+
console.print("[dim]Running task non-interactively...[/dim]")
|
|
52
|
+
if model:
|
|
53
|
+
console.print(f"[dim]Model: {model}[/dim]")
|
|
54
|
+
console.print()
|
|
55
|
+
|
|
56
|
+
# Load MCP tools if ~/.icarus/mcp.json exists
|
|
57
|
+
mcp_manager = MCPManager.from_config_file(ctx.config_dir / "mcp.json")
|
|
58
|
+
mcp_tools, _ = await mcp_manager.start() if mcp_manager else ([], [])
|
|
59
|
+
|
|
60
|
+
if mcp_tools and not quiet:
|
|
61
|
+
names = ", ".join(mcp_manager.server_names)
|
|
62
|
+
console.print(f"[dim]MCP: {len(mcp_tools)} tools from {names}[/dim]")
|
|
63
|
+
|
|
64
|
+
result = create_icarus_agent(
|
|
65
|
+
ctx,
|
|
66
|
+
model=model,
|
|
67
|
+
tools=mcp_tools or None,
|
|
68
|
+
auto_approve=auto_approve,
|
|
69
|
+
)
|
|
70
|
+
llm_model = result.model
|
|
71
|
+
|
|
72
|
+
async for event in stream_turn(
|
|
73
|
+
agent=result.graph,
|
|
74
|
+
user_input=message,
|
|
75
|
+
thread_id=thread_id,
|
|
76
|
+
assistant_id="default",
|
|
77
|
+
auto_approve=auto_approve,
|
|
78
|
+
):
|
|
79
|
+
match event:
|
|
80
|
+
case TextDelta(text=text):
|
|
81
|
+
sys.stdout.write(text)
|
|
82
|
+
sys.stdout.flush()
|
|
83
|
+
case _:
|
|
84
|
+
pass # Headless ignores non-text events
|
|
85
|
+
|
|
86
|
+
sys.stdout.write("\n")
|
|
87
|
+
sys.stdout.flush()
|
|
88
|
+
|
|
89
|
+
if not quiet:
|
|
90
|
+
console.print()
|
|
91
|
+
console.print("[green]Task completed[/green]")
|
|
92
|
+
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
except KeyboardInterrupt:
|
|
96
|
+
console.print("\n[yellow]Interrupted[/yellow]")
|
|
97
|
+
return 130
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.exception("Error during headless execution")
|
|
100
|
+
console.print(f"\n[red]Error ({type(e).__name__}): {e}[/red]")
|
|
101
|
+
return 1
|
|
102
|
+
finally:
|
|
103
|
+
if mcp_manager is not None:
|
|
104
|
+
await mcp_manager.stop()
|
|
105
|
+
if llm_model is not None and hasattr(llm_model, "aclose"):
|
|
106
|
+
await llm_model.aclose()
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""CLI entry point — argparse + mode dispatch.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
icarus → Textual TUI (interactive)
|
|
5
|
+
icarus run "fix the bug" → Headless mode
|
|
6
|
+
icarus --model azure_ai:Kimi-K2.5 → Override model
|
|
7
|
+
icarus --auto-approve → Skip HITL
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import asyncio
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _make_help_action(
|
|
21
|
+
print_help: Callable[[], None],
|
|
22
|
+
) -> type[argparse.Action]:
|
|
23
|
+
"""Create an argparse Action that prints custom help and exits."""
|
|
24
|
+
|
|
25
|
+
class _HelpAction(argparse.Action):
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
option_strings,
|
|
29
|
+
dest=argparse.SUPPRESS,
|
|
30
|
+
default=argparse.SUPPRESS,
|
|
31
|
+
**kwargs,
|
|
32
|
+
):
|
|
33
|
+
super().__init__(
|
|
34
|
+
option_strings=option_strings,
|
|
35
|
+
dest=dest,
|
|
36
|
+
default=default,
|
|
37
|
+
nargs=0,
|
|
38
|
+
**kwargs,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
42
|
+
print_help()
|
|
43
|
+
parser.exit()
|
|
44
|
+
|
|
45
|
+
return _HelpAction
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
49
|
+
"""Build the argument parser."""
|
|
50
|
+
parser = argparse.ArgumentParser(
|
|
51
|
+
prog="icarus",
|
|
52
|
+
description="ICARUS — AI Coding Agent",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--model", "-M",
|
|
57
|
+
type=str,
|
|
58
|
+
default=None,
|
|
59
|
+
help="Model override (e.g. 'azure_ai:Kimi-K2.5')",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--auto-approve",
|
|
63
|
+
action="store_true",
|
|
64
|
+
default=False,
|
|
65
|
+
help="Skip HITL approval for all tool calls",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--debug",
|
|
69
|
+
action="store_true",
|
|
70
|
+
default=False,
|
|
71
|
+
help="Enable debug logging",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--quiet", "-q",
|
|
75
|
+
action="store_true",
|
|
76
|
+
default=False,
|
|
77
|
+
help="Quiet mode (stderr for status, stdout for response only)",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
81
|
+
|
|
82
|
+
run_parser = subparsers.add_parser("run", help="Run a task non-interactively")
|
|
83
|
+
run_parser.add_argument("message", type=str, help="The task message to execute")
|
|
84
|
+
|
|
85
|
+
subparsers.add_parser("models", help="List available models")
|
|
86
|
+
|
|
87
|
+
from icarus_cli.skills_commands import setup_skills_parser
|
|
88
|
+
|
|
89
|
+
setup_skills_parser(subparsers, make_help_action=_make_help_action)
|
|
90
|
+
|
|
91
|
+
# Server commands
|
|
92
|
+
setup_p = subparsers.add_parser("setup", help="Configure the Icarus server")
|
|
93
|
+
setup_p.add_argument("--non-interactive", action="store_true")
|
|
94
|
+
setup_p.add_argument("--port", type=int, default=8642)
|
|
95
|
+
setup_p.add_argument("--bind", default="0.0.0.0")
|
|
96
|
+
setup_p.add_argument("--admin-username", default=None)
|
|
97
|
+
setup_p.add_argument("--admin-password", default=None)
|
|
98
|
+
setup_p.add_argument("--daemon", choices=["user", "system", "none"], default="none")
|
|
99
|
+
setup_p.add_argument("--skip-health", action="store_true")
|
|
100
|
+
|
|
101
|
+
serve_p = subparsers.add_parser("serve", help="Start the Icarus HTTP server")
|
|
102
|
+
serve_p.add_argument("--port", type=int, default=None)
|
|
103
|
+
serve_p.add_argument("--bind", default=None)
|
|
104
|
+
|
|
105
|
+
subparsers.add_parser("start", help="Start Icarus daemon (Linux systemd)")
|
|
106
|
+
subparsers.add_parser("stop", help="Stop Icarus daemon (Linux systemd)")
|
|
107
|
+
|
|
108
|
+
# User management
|
|
109
|
+
user_p = subparsers.add_parser("user", help="Manage server users")
|
|
110
|
+
user_sub = user_p.add_subparsers(dest="user_command")
|
|
111
|
+
|
|
112
|
+
user_add = user_sub.add_parser("add", help="Create a new user")
|
|
113
|
+
user_add.add_argument("username")
|
|
114
|
+
user_add.add_argument("--email", required=True)
|
|
115
|
+
user_add.add_argument("--role", choices=["user", "admin", "read-only"], default="user")
|
|
116
|
+
|
|
117
|
+
user_list_p = user_sub.add_parser("list", help="List all users")
|
|
118
|
+
user_list_p.add_argument("--pending", action="store_true")
|
|
119
|
+
|
|
120
|
+
user_reset = user_sub.add_parser("reset-password", help="Reset a user's password")
|
|
121
|
+
user_reset.add_argument("username")
|
|
122
|
+
|
|
123
|
+
user_role = user_sub.add_parser("set-role", help="Change a user's role")
|
|
124
|
+
user_role.add_argument("username")
|
|
125
|
+
user_role.add_argument("role", choices=["admin", "user", "read-only", "pending"])
|
|
126
|
+
|
|
127
|
+
user_revoke = user_sub.add_parser("revoke", help="Revoke all sessions")
|
|
128
|
+
user_revoke.add_argument("username")
|
|
129
|
+
|
|
130
|
+
user_unlock_p = user_sub.add_parser("unlock", help="Unlock a locked account")
|
|
131
|
+
user_unlock_p.add_argument("username")
|
|
132
|
+
|
|
133
|
+
user_remove = user_sub.add_parser("remove", help="Remove a user")
|
|
134
|
+
user_remove.add_argument("username")
|
|
135
|
+
|
|
136
|
+
return parser
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _require_serve() -> None:
|
|
140
|
+
"""Exit with a helpful message if the [serve] extra is not installed."""
|
|
141
|
+
try:
|
|
142
|
+
import icarus_server # noqa: F401
|
|
143
|
+
except ImportError:
|
|
144
|
+
print("This command requires the [serve] extra. Install with: pip install icarus[serve]")
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _systemctl_cmd(action: str) -> None:
|
|
149
|
+
"""Run systemctl start/stop for the icarus daemon."""
|
|
150
|
+
import platform
|
|
151
|
+
import subprocess
|
|
152
|
+
|
|
153
|
+
if platform.system() != "Linux":
|
|
154
|
+
print("Daemon mode requires Linux with systemd. Use 'icarus serve' for foreground.")
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
_require_serve()
|
|
157
|
+
from icarus_server.config import read_config
|
|
158
|
+
|
|
159
|
+
config = read_config()
|
|
160
|
+
if not config or config.daemon_mode == "none":
|
|
161
|
+
print("No daemon configured. Run 'icarus setup' and choose user/system service.")
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
cmd = ["systemctl"]
|
|
164
|
+
if config.daemon_mode == "user":
|
|
165
|
+
cmd.append("--user")
|
|
166
|
+
cmd.extend([action, "icarus"])
|
|
167
|
+
subprocess.run(cmd, check=True)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _handle_user_command(args: "argparse.Namespace") -> None:
|
|
171
|
+
"""Dispatch icarus user sub-commands."""
|
|
172
|
+
from icarus_server.auth.db import get_user_by_email, get_user_by_username, list_users, delete_user
|
|
173
|
+
from icarus_server.auth.local import LocalAuthProvider
|
|
174
|
+
from icarus_server.config import SERVER_DIR
|
|
175
|
+
|
|
176
|
+
db_path = SERVER_DIR / "server.db"
|
|
177
|
+
if not db_path.exists():
|
|
178
|
+
print("No server.db found. Run 'icarus setup' first.")
|
|
179
|
+
sys.exit(1)
|
|
180
|
+
|
|
181
|
+
provider = LocalAuthProvider(db_path)
|
|
182
|
+
|
|
183
|
+
if args.user_command == "add":
|
|
184
|
+
from getpass import getpass
|
|
185
|
+
password = getpass(f"Password for {args.username}: ")
|
|
186
|
+
user = provider.register(
|
|
187
|
+
username=args.username, email=args.email,
|
|
188
|
+
password=password, role=args.role,
|
|
189
|
+
)
|
|
190
|
+
print(f"User '{args.username}' created (id: {user.id})")
|
|
191
|
+
|
|
192
|
+
elif args.user_command == "list":
|
|
193
|
+
role_filter = "pending" if args.pending else None
|
|
194
|
+
users = list_users(db_path, role=role_filter)
|
|
195
|
+
if not users:
|
|
196
|
+
print("No users found.")
|
|
197
|
+
return
|
|
198
|
+
print(f"{'USERNAME':<20} {'ROLE':<12} {'EMAIL':<30} {'PROVIDER':<10}")
|
|
199
|
+
for u in users:
|
|
200
|
+
print(f"{u.username:<20} {u.role:<12} {u.email:<30} {u.auth_provider:<10}")
|
|
201
|
+
|
|
202
|
+
elif args.user_command == "reset-password":
|
|
203
|
+
from getpass import getpass
|
|
204
|
+
user = get_user_by_username(db_path, args.username) or get_user_by_email(db_path, args.username)
|
|
205
|
+
if not user:
|
|
206
|
+
print(f"User '{args.username}' not found.")
|
|
207
|
+
sys.exit(1)
|
|
208
|
+
password = getpass("New password: ")
|
|
209
|
+
provider.update_password(user.id, password)
|
|
210
|
+
print(f"Password updated for '{args.username}'")
|
|
211
|
+
|
|
212
|
+
elif args.user_command == "set-role":
|
|
213
|
+
user = get_user_by_username(db_path, args.username)
|
|
214
|
+
if not user:
|
|
215
|
+
print(f"User '{args.username}' not found.")
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
provider.set_role(user.id, args.role)
|
|
218
|
+
print(f"Role updated to '{args.role}' for '{args.username}'")
|
|
219
|
+
|
|
220
|
+
elif args.user_command == "revoke":
|
|
221
|
+
user = get_user_by_username(db_path, args.username)
|
|
222
|
+
if not user:
|
|
223
|
+
print(f"User '{args.username}' not found.")
|
|
224
|
+
sys.exit(1)
|
|
225
|
+
provider.revoke_all_sessions(user.id)
|
|
226
|
+
print(f"All sessions revoked for '{args.username}'")
|
|
227
|
+
|
|
228
|
+
elif args.user_command == "unlock":
|
|
229
|
+
print("Account lockouts are in-memory. Restart 'icarus serve' to clear all lockouts.")
|
|
230
|
+
print("For persistent lockout clearing, the admin API will be added in a future version.")
|
|
231
|
+
|
|
232
|
+
elif args.user_command == "remove":
|
|
233
|
+
user = get_user_by_username(db_path, args.username)
|
|
234
|
+
if not user:
|
|
235
|
+
print(f"User '{args.username}' not found.")
|
|
236
|
+
sys.exit(1)
|
|
237
|
+
delete_user(db_path, user.id)
|
|
238
|
+
print(f"User '{args.username}' removed")
|
|
239
|
+
|
|
240
|
+
else:
|
|
241
|
+
print("Usage: icarus user {add|list|reset-password|set-role|revoke|unlock|remove}")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def main() -> None:
|
|
245
|
+
"""Main entry point for the ICARUS CLI."""
|
|
246
|
+
parser = build_parser()
|
|
247
|
+
args = parser.parse_args()
|
|
248
|
+
|
|
249
|
+
log_level = logging.DEBUG if args.debug else logging.WARNING
|
|
250
|
+
logging.basicConfig(
|
|
251
|
+
level=log_level,
|
|
252
|
+
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if args.command == "run":
|
|
256
|
+
from icarus_cli.headless import run_headless
|
|
257
|
+
|
|
258
|
+
exit_code = asyncio.run(
|
|
259
|
+
run_headless(
|
|
260
|
+
args.message,
|
|
261
|
+
model=args.model,
|
|
262
|
+
auto_approve=args.auto_approve,
|
|
263
|
+
quiet=args.quiet,
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
sys.exit(exit_code)
|
|
267
|
+
|
|
268
|
+
elif args.command == "models":
|
|
269
|
+
from icarus.config.model_config import ModelConfig, get_available_models
|
|
270
|
+
|
|
271
|
+
config = ModelConfig.load()
|
|
272
|
+
available = get_available_models()
|
|
273
|
+
default = config.default_model or config.recent_model
|
|
274
|
+
models = [
|
|
275
|
+
f"{provider}:{model}"
|
|
276
|
+
for provider, model_list in sorted(available.items())
|
|
277
|
+
for model in model_list
|
|
278
|
+
]
|
|
279
|
+
if models:
|
|
280
|
+
if default:
|
|
281
|
+
print(f"Default: {default}")
|
|
282
|
+
print("\nAvailable models:")
|
|
283
|
+
for m in models:
|
|
284
|
+
marker = " (default)" if m == default else ""
|
|
285
|
+
print(f" - {m}{marker}")
|
|
286
|
+
else:
|
|
287
|
+
print("No models configured. Add providers to ~/.icarus/config.toml")
|
|
288
|
+
|
|
289
|
+
elif args.command == "skills":
|
|
290
|
+
from icarus_cli.skills_commands import execute_skills_command
|
|
291
|
+
|
|
292
|
+
execute_skills_command(args)
|
|
293
|
+
|
|
294
|
+
elif args.command in ("setup", "user", "serve"):
|
|
295
|
+
try:
|
|
296
|
+
import icarus_server # noqa: F401
|
|
297
|
+
except ImportError:
|
|
298
|
+
print("Server features require: pip install icarus[serve]", file=sys.stderr)
|
|
299
|
+
sys.exit(1)
|
|
300
|
+
|
|
301
|
+
if args.command == "setup":
|
|
302
|
+
if args.non_interactive:
|
|
303
|
+
from icarus_server.setup_wizard import run_setup_non_interactive
|
|
304
|
+
|
|
305
|
+
admin_password = args.admin_password or os.environ.get("ICARUS_ADMIN_PASSWORD")
|
|
306
|
+
run_setup_non_interactive(
|
|
307
|
+
port=args.port, bind=args.bind,
|
|
308
|
+
admin_username=args.admin_username,
|
|
309
|
+
admin_password=admin_password,
|
|
310
|
+
daemon=args.daemon, skip_health=args.skip_health,
|
|
311
|
+
)
|
|
312
|
+
else:
|
|
313
|
+
from icarus_server.setup_wizard import run_setup_interactive
|
|
314
|
+
|
|
315
|
+
run_setup_interactive()
|
|
316
|
+
|
|
317
|
+
elif args.command == "user":
|
|
318
|
+
_handle_user_command(args)
|
|
319
|
+
|
|
320
|
+
elif args.command == "serve":
|
|
321
|
+
from icarus_server.config import read_config
|
|
322
|
+
|
|
323
|
+
config = read_config()
|
|
324
|
+
if not config:
|
|
325
|
+
print("No server.toml found. Run 'icarus setup' first.")
|
|
326
|
+
sys.exit(1)
|
|
327
|
+
if args.port:
|
|
328
|
+
config.port = args.port
|
|
329
|
+
if args.bind:
|
|
330
|
+
config.bind = args.bind
|
|
331
|
+
from icarus_server.app import create_app
|
|
332
|
+
|
|
333
|
+
import uvicorn
|
|
334
|
+
|
|
335
|
+
app = create_app(config)
|
|
336
|
+
uvicorn.run(app, host=config.bind, port=config.port, ws_per_message_deflate=False)
|
|
337
|
+
|
|
338
|
+
elif args.command in ("start", "stop"):
|
|
339
|
+
_systemctl_cmd(args.command)
|
|
340
|
+
|
|
341
|
+
else:
|
|
342
|
+
# Interactive TUI mode
|
|
343
|
+
asyncio.run(_run_tui(args))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
async def _run_tui(args: argparse.Namespace) -> None:
|
|
347
|
+
"""Launch the ICARUS Textual TUI."""
|
|
348
|
+
try:
|
|
349
|
+
from icarus_tui.app import run_textual_app
|
|
350
|
+
except ImportError:
|
|
351
|
+
print("TUI requires: pip install icarus[tui]", file=sys.stderr)
|
|
352
|
+
sys.exit(1)
|
|
353
|
+
|
|
354
|
+
from icarus.mcp import MCPManager
|
|
355
|
+
from icarus.runtime.builder import create_icarus_agent
|
|
356
|
+
from icarus.runtime.context import UserContext
|
|
357
|
+
from icarus.session.threads import get_checkpointer
|
|
358
|
+
|
|
359
|
+
ctx = UserContext.default()
|
|
360
|
+
|
|
361
|
+
# Load MCP tools if ~/.icarus/mcp.json exists
|
|
362
|
+
mcp_manager = MCPManager.from_config_file(ctx.config_dir / "mcp.json")
|
|
363
|
+
mcp_tools, _ = await mcp_manager.start() if mcp_manager else ([], [])
|
|
364
|
+
|
|
365
|
+
# Use SQLite checkpointer for session persistence across restarts
|
|
366
|
+
async with get_checkpointer() as checkpointer:
|
|
367
|
+
result = create_icarus_agent(
|
|
368
|
+
ctx,
|
|
369
|
+
model=args.model,
|
|
370
|
+
tools=mcp_tools or None,
|
|
371
|
+
auto_approve=args.auto_approve,
|
|
372
|
+
checkpointer=checkpointer,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
app_result = await run_textual_app(
|
|
377
|
+
agent=result.graph,
|
|
378
|
+
backend=result.backend,
|
|
379
|
+
auto_approve=args.auto_approve,
|
|
380
|
+
cwd=str(ctx.working_dir),
|
|
381
|
+
checkpointer=checkpointer,
|
|
382
|
+
ctx=ctx,
|
|
383
|
+
mcp_tools=mcp_tools,
|
|
384
|
+
)
|
|
385
|
+
if app_result.return_code != 0:
|
|
386
|
+
sys.exit(app_result.return_code)
|
|
387
|
+
finally:
|
|
388
|
+
if mcp_manager:
|
|
389
|
+
await mcp_manager.stop()
|
|
390
|
+
if hasattr(result.model, "aclose"):
|
|
391
|
+
await result.model.aclose()
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
if __name__ == "__main__":
|
|
395
|
+
main()
|