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.
Files changed (171) hide show
  1. icarus_agent-1.0.0/PKG-INFO +84 -0
  2. icarus_agent-1.0.0/README.md +16 -0
  3. icarus_agent-1.0.0/cli/__init__.py +1 -0
  4. icarus_agent-1.0.0/cli/headless.py +106 -0
  5. icarus_agent-1.0.0/cli/main.py +395 -0
  6. icarus_agent-1.0.0/cli/skills_commands.py +830 -0
  7. icarus_agent-1.0.0/cli/ui.py +149 -0
  8. icarus_agent-1.0.0/core/__init__.py +5 -0
  9. icarus_agent-1.0.0/core/_version.py +3 -0
  10. icarus_agent-1.0.0/core/assets/default_agent_prompt.md +12 -0
  11. icarus_agent-1.0.0/core/assets/memory_instructions.md +27 -0
  12. icarus_agent-1.0.0/core/assets/pim_instructions.md +33 -0
  13. icarus_agent-1.0.0/core/assets/system_prompt.md +232 -0
  14. icarus_agent-1.0.0/core/built_in_skills/__init__.py +5 -0
  15. icarus_agent-1.0.0/core/built_in_skills/doc-coauthoring/SKILL.md +375 -0
  16. icarus_agent-1.0.0/core/built_in_skills/docx/SKILL.md +196 -0
  17. icarus_agent-1.0.0/core/built_in_skills/docx/docx-js.md +350 -0
  18. icarus_agent-1.0.0/core/built_in_skills/docx/ooxml.md +610 -0
  19. icarus_agent-1.0.0/core/built_in_skills/frontend-design/SKILL.md +41 -0
  20. icarus_agent-1.0.0/core/built_in_skills/pdf/SKILL.md +293 -0
  21. icarus_agent-1.0.0/core/built_in_skills/pdf/forms.md +205 -0
  22. icarus_agent-1.0.0/core/built_in_skills/pdf/reference.md +612 -0
  23. icarus_agent-1.0.0/core/built_in_skills/pptx/SKILL.md +483 -0
  24. icarus_agent-1.0.0/core/built_in_skills/pptx/html2pptx.md +625 -0
  25. icarus_agent-1.0.0/core/built_in_skills/pptx/ooxml.md +427 -0
  26. icarus_agent-1.0.0/core/built_in_skills/skill-creator/SKILL.md +399 -0
  27. icarus_agent-1.0.0/core/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  28. icarus_agent-1.0.0/core/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  29. icarus_agent-1.0.0/core/built_in_skills/theme-factory/SKILL.md +39 -0
  30. icarus_agent-1.0.0/core/built_in_skills/xlsx/SKILL.md +288 -0
  31. icarus_agent-1.0.0/core/built_in_skills/xlsx/recalc.py +178 -0
  32. icarus_agent-1.0.0/core/config/__init__.py +1 -0
  33. icarus_agent-1.0.0/core/config/app_config.py +1345 -0
  34. icarus_agent-1.0.0/core/config/display.py +248 -0
  35. icarus_agent-1.0.0/core/config/model_config.py +1212 -0
  36. icarus_agent-1.0.0/core/config/ollama.py +98 -0
  37. icarus_agent-1.0.0/core/config/permissions.py +96 -0
  38. icarus_agent-1.0.0/core/engine/__init__.py +12 -0
  39. icarus_agent-1.0.0/core/engine/engine.py +528 -0
  40. icarus_agent-1.0.0/core/integrations/__init__.py +1 -0
  41. icarus_agent-1.0.0/core/integrations/daytona.py +218 -0
  42. icarus_agent-1.0.0/core/integrations/langsmith.py +282 -0
  43. icarus_agent-1.0.0/core/integrations/modal.py +223 -0
  44. icarus_agent-1.0.0/core/integrations/runloop.py +225 -0
  45. icarus_agent-1.0.0/core/integrations/sandbox_factory.py +184 -0
  46. icarus_agent-1.0.0/core/integrations/sandbox_provider.py +71 -0
  47. icarus_agent-1.0.0/core/mcp/__init__.py +29 -0
  48. icarus_agent-1.0.0/core/mcp/client.py +375 -0
  49. icarus_agent-1.0.0/core/mcp/config.py +144 -0
  50. icarus_agent-1.0.0/core/pim/__init__.py +36 -0
  51. icarus_agent-1.0.0/core/pim/calendar.py +240 -0
  52. icarus_agent-1.0.0/core/pim/common.py +168 -0
  53. icarus_agent-1.0.0/core/pim/contacts.py +248 -0
  54. icarus_agent-1.0.0/core/pim/todos.py +309 -0
  55. icarus_agent-1.0.0/core/runtime/__init__.py +9 -0
  56. icarus_agent-1.0.0/core/runtime/agent.py +673 -0
  57. icarus_agent-1.0.0/core/runtime/backend.py +137 -0
  58. icarus_agent-1.0.0/core/runtime/builder.py +123 -0
  59. icarus_agent-1.0.0/core/runtime/context.py +243 -0
  60. icarus_agent-1.0.0/core/runtime/local_context.py +517 -0
  61. icarus_agent-1.0.0/core/runtime/memory_context.py +51 -0
  62. icarus_agent-1.0.0/core/runtime/pim_context.py +161 -0
  63. icarus_agent-1.0.0/core/runtime/subagents.py +173 -0
  64. icarus_agent-1.0.0/core/runtime/time_context.py +120 -0
  65. icarus_agent-1.0.0/core/session/__init__.py +33 -0
  66. icarus_agent-1.0.0/core/session/events.py +113 -0
  67. icarus_agent-1.0.0/core/session/model.py +256 -0
  68. icarus_agent-1.0.0/core/session/search.py +438 -0
  69. icarus_agent-1.0.0/core/session/serialize.py +49 -0
  70. icarus_agent-1.0.0/core/session/stream.py +487 -0
  71. icarus_agent-1.0.0/core/session/thread_state.py +108 -0
  72. icarus_agent-1.0.0/core/session/threads.py +436 -0
  73. icarus_agent-1.0.0/core/session/titles.py +79 -0
  74. icarus_agent-1.0.0/core/skills/__init__.py +5 -0
  75. icarus_agent-1.0.0/core/skills/installer.py +238 -0
  76. icarus_agent-1.0.0/core/skills/load.py +223 -0
  77. icarus_agent-1.0.0/core/skills/registry.py +133 -0
  78. icarus_agent-1.0.0/core/skills/registry_config.py +92 -0
  79. icarus_agent-1.0.0/core/skills/validation.py +113 -0
  80. icarus_agent-1.0.0/core/tools/__init__.py +251 -0
  81. icarus_agent-1.0.0/core/tools/browser.py +545 -0
  82. icarus_agent-1.0.0/core/tools/pim.py +448 -0
  83. icarus_agent-1.0.0/core/utils/__init__.py +1 -0
  84. icarus_agent-1.0.0/core/utils/clipboard.py +128 -0
  85. icarus_agent-1.0.0/core/utils/file_ops.py +472 -0
  86. icarus_agent-1.0.0/core/utils/image_utils.py +100 -0
  87. icarus_agent-1.0.0/core/utils/input.py +286 -0
  88. icarus_agent-1.0.0/core/utils/toml.py +44 -0
  89. icarus_agent-1.0.0/core/utils/tool_display.py +273 -0
  90. icarus_agent-1.0.0/desktop/sidecar/__init__.py +1 -0
  91. icarus_agent-1.0.0/desktop/sidecar/__main__.py +5 -0
  92. icarus_agent-1.0.0/desktop/sidecar/main.py +1203 -0
  93. icarus_agent-1.0.0/desktop/sidecar/protocol.py +103 -0
  94. icarus_agent-1.0.0/icarus_agent.egg-info/PKG-INFO +84 -0
  95. icarus_agent-1.0.0/icarus_agent.egg-info/SOURCES.txt +170 -0
  96. icarus_agent-1.0.0/icarus_agent.egg-info/dependency_links.txt +1 -0
  97. icarus_agent-1.0.0/icarus_agent.egg-info/entry_points.txt +3 -0
  98. icarus_agent-1.0.0/icarus_agent.egg-info/requires.txt +74 -0
  99. icarus_agent-1.0.0/icarus_agent.egg-info/top_level.txt +4 -0
  100. icarus_agent-1.0.0/pyproject.toml +82 -0
  101. icarus_agent-1.0.0/server/__init__.py +0 -0
  102. icarus_agent-1.0.0/server/__main__.py +28 -0
  103. icarus_agent-1.0.0/server/app.py +232 -0
  104. icarus_agent-1.0.0/server/auth/__init__.py +1 -0
  105. icarus_agent-1.0.0/server/auth/audit.py +56 -0
  106. icarus_agent-1.0.0/server/auth/base.py +17 -0
  107. icarus_agent-1.0.0/server/auth/db.py +138 -0
  108. icarus_agent-1.0.0/server/auth/local.py +74 -0
  109. icarus_agent-1.0.0/server/auth/middleware.py +33 -0
  110. icarus_agent-1.0.0/server/auth/rate_limit.py +73 -0
  111. icarus_agent-1.0.0/server/auth/routes.py +131 -0
  112. icarus_agent-1.0.0/server/auth/tokens.py +125 -0
  113. icarus_agent-1.0.0/server/config.py +82 -0
  114. icarus_agent-1.0.0/server/engine_pool.py +174 -0
  115. icarus_agent-1.0.0/server/handler.py +769 -0
  116. icarus_agent-1.0.0/server/setup_wizard.py +300 -0
  117. icarus_agent-1.0.0/server/users.py +16 -0
  118. icarus_agent-1.0.0/server/ws.py +93 -0
  119. icarus_agent-1.0.0/setup.cfg +48 -0
  120. icarus_agent-1.0.0/tests/test_agent_e2e.py +89 -0
  121. icarus_agent-1.0.0/tests/test_builder.py +140 -0
  122. icarus_agent-1.0.0/tests/test_contacts_migration.py +146 -0
  123. icarus_agent-1.0.0/tests/test_context.py +105 -0
  124. icarus_agent-1.0.0/tests/test_mcp_config.py +228 -0
  125. icarus_agent-1.0.0/tests/test_mcp_resilience.py +472 -0
  126. icarus_agent-1.0.0/tests/test_mcp_server_death.py +232 -0
  127. icarus_agent-1.0.0/tests/test_memory_context.py +48 -0
  128. icarus_agent-1.0.0/tests/test_model_switch.py +432 -0
  129. icarus_agent-1.0.0/tests/test_models.py +216 -0
  130. icarus_agent-1.0.0/tests/test_ollama_discovery.py +363 -0
  131. icarus_agent-1.0.0/tests/test_package_extras.py +65 -0
  132. icarus_agent-1.0.0/tests/test_permissioned_backend.py +196 -0
  133. icarus_agent-1.0.0/tests/test_permissions.py +151 -0
  134. icarus_agent-1.0.0/tests/test_permissions_wiring.py +60 -0
  135. icarus_agent-1.0.0/tests/test_pim_calendar.py +220 -0
  136. icarus_agent-1.0.0/tests/test_pim_contacts.py +198 -0
  137. icarus_agent-1.0.0/tests/test_pim_context.py +157 -0
  138. icarus_agent-1.0.0/tests/test_pim_todos.py +250 -0
  139. icarus_agent-1.0.0/tests/test_pim_tools.py +239 -0
  140. icarus_agent-1.0.0/tests/test_provider_wiring.py +181 -0
  141. icarus_agent-1.0.0/tests/test_search.py +560 -0
  142. icarus_agent-1.0.0/tests/test_session_model.py +269 -0
  143. icarus_agent-1.0.0/tests/test_session_stream.py +573 -0
  144. icarus_agent-1.0.0/tests/test_sidecar.py +412 -0
  145. icarus_agent-1.0.0/tests/test_sidecar_concurrent.py +83 -0
  146. icarus_agent-1.0.0/tests/test_skill_store.py +175 -0
  147. icarus_agent-1.0.0/tests/test_thread_state.py +213 -0
  148. icarus_agent-1.0.0/tests/test_threads.py +244 -0
  149. icarus_agent-1.0.0/tests/test_time_context.py +116 -0
  150. icarus_agent-1.0.0/tui/__init__.py +1 -0
  151. icarus_agent-1.0.0/tui/adapter.py +179 -0
  152. icarus_agent-1.0.0/tui/app.py +2204 -0
  153. icarus_agent-1.0.0/tui/app.tcss +181 -0
  154. icarus_agent-1.0.0/tui/widgets/__init__.py +9 -0
  155. icarus_agent-1.0.0/tui/widgets/_links.py +38 -0
  156. icarus_agent-1.0.0/tui/widgets/approval.py +334 -0
  157. icarus_agent-1.0.0/tui/widgets/autocomplete.py +633 -0
  158. icarus_agent-1.0.0/tui/widgets/chat_input.py +1303 -0
  159. icarus_agent-1.0.0/tui/widgets/diff.py +215 -0
  160. icarus_agent-1.0.0/tui/widgets/history.py +160 -0
  161. icarus_agent-1.0.0/tui/widgets/loading.py +172 -0
  162. icarus_agent-1.0.0/tui/widgets/message_store.py +611 -0
  163. icarus_agent-1.0.0/tui/widgets/messages.py +1324 -0
  164. icarus_agent-1.0.0/tui/widgets/model_selector.py +565 -0
  165. icarus_agent-1.0.0/tui/widgets/navigable_list.py +101 -0
  166. icarus_agent-1.0.0/tui/widgets/skill_store.py +983 -0
  167. icarus_agent-1.0.0/tui/widgets/status.py +279 -0
  168. icarus_agent-1.0.0/tui/widgets/thread_selector.py +480 -0
  169. icarus_agent-1.0.0/tui/widgets/tool_renderers.py +128 -0
  170. icarus_agent-1.0.0/tui/widgets/tool_widgets.py +245 -0
  171. 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()