iac-code 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. iac_code/__init__.py +2 -0
  2. iac_code/acp/__init__.py +97 -0
  3. iac_code/acp/convert.py +423 -0
  4. iac_code/acp/http_sse.py +448 -0
  5. iac_code/acp/mcp.py +54 -0
  6. iac_code/acp/metrics.py +71 -0
  7. iac_code/acp/server.py +662 -0
  8. iac_code/acp/session.py +446 -0
  9. iac_code/acp/slash_registry.py +125 -0
  10. iac_code/acp/state.py +99 -0
  11. iac_code/acp/tools.py +112 -0
  12. iac_code/acp/types.py +13 -0
  13. iac_code/acp/version.py +26 -0
  14. iac_code/agent/__init__.py +19 -0
  15. iac_code/agent/agent_loop.py +640 -0
  16. iac_code/agent/agent_tool.py +269 -0
  17. iac_code/agent/agent_types.py +87 -0
  18. iac_code/agent/message.py +153 -0
  19. iac_code/agent/system_prompt.py +313 -0
  20. iac_code/cli/__init__.py +3 -0
  21. iac_code/cli/headless.py +114 -0
  22. iac_code/cli/main.py +246 -0
  23. iac_code/cli/output_formats.py +125 -0
  24. iac_code/commands/__init__.py +93 -0
  25. iac_code/commands/auth.py +1055 -0
  26. iac_code/commands/clear.py +34 -0
  27. iac_code/commands/compact.py +43 -0
  28. iac_code/commands/debug.py +45 -0
  29. iac_code/commands/effort.py +116 -0
  30. iac_code/commands/exit.py +10 -0
  31. iac_code/commands/help.py +49 -0
  32. iac_code/commands/model.py +130 -0
  33. iac_code/commands/registry.py +245 -0
  34. iac_code/commands/resume.py +49 -0
  35. iac_code/commands/tasks.py +41 -0
  36. iac_code/config.py +304 -0
  37. iac_code/i18n/__init__.py +141 -0
  38. iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +1355 -0
  39. iac_code/memory/__init__.py +1 -0
  40. iac_code/memory/memory_manager.py +92 -0
  41. iac_code/memory/memory_tools.py +88 -0
  42. iac_code/providers/__init__.py +1 -0
  43. iac_code/providers/anthropic_provider.py +284 -0
  44. iac_code/providers/base.py +128 -0
  45. iac_code/providers/dashscope_provider.py +47 -0
  46. iac_code/providers/deepseek_provider.py +36 -0
  47. iac_code/providers/manager.py +399 -0
  48. iac_code/providers/openai_provider.py +344 -0
  49. iac_code/providers/retry.py +58 -0
  50. iac_code/providers/stream_watchdog.py +47 -0
  51. iac_code/providers/thinking.py +164 -0
  52. iac_code/services/__init__.py +1 -0
  53. iac_code/services/agent_factory.py +127 -0
  54. iac_code/services/cloud_credentials.py +22 -0
  55. iac_code/services/context_manager.py +221 -0
  56. iac_code/services/providers/__init__.py +1 -0
  57. iac_code/services/providers/aliyun.py +232 -0
  58. iac_code/services/session_index.py +281 -0
  59. iac_code/services/session_storage.py +245 -0
  60. iac_code/services/telemetry/__init__.py +66 -0
  61. iac_code/services/telemetry/attributes.py +84 -0
  62. iac_code/services/telemetry/client.py +330 -0
  63. iac_code/services/telemetry/config.py +76 -0
  64. iac_code/services/telemetry/constants.py +75 -0
  65. iac_code/services/telemetry/content_serializer.py +124 -0
  66. iac_code/services/telemetry/events.py +42 -0
  67. iac_code/services/telemetry/fallback.py +59 -0
  68. iac_code/services/telemetry/identity.py +73 -0
  69. iac_code/services/telemetry/metrics.py +62 -0
  70. iac_code/services/telemetry/names.py +199 -0
  71. iac_code/services/telemetry/sanitize.py +88 -0
  72. iac_code/services/telemetry/sink.py +67 -0
  73. iac_code/services/telemetry/tracing.py +38 -0
  74. iac_code/services/telemetry/types.py +13 -0
  75. iac_code/services/token_budget.py +54 -0
  76. iac_code/services/token_counter.py +76 -0
  77. iac_code/skills/__init__.py +1 -0
  78. iac_code/skills/bundled/__init__.py +94 -0
  79. iac_code/skills/bundled/iac_aliyun/SKILL.md +192 -0
  80. iac_code/skills/bundled/iac_aliyun/__init__.py +16 -0
  81. iac_code/skills/bundled/iac_aliyun/references/cloud-products/ecs.md +167 -0
  82. iac_code/skills/bundled/iac_aliyun/references/cloud-products/oss.md +69 -0
  83. iac_code/skills/bundled/iac_aliyun/references/cloud-products/rds.md +95 -0
  84. iac_code/skills/bundled/iac_aliyun/references/cloud-products/redis.md +100 -0
  85. iac_code/skills/bundled/iac_aliyun/references/cloud-products/slb.md +60 -0
  86. iac_code/skills/bundled/iac_aliyun/references/cloud-products/vpc.md +54 -0
  87. iac_code/skills/bundled/iac_aliyun/references/ros-template.md +155 -0
  88. iac_code/skills/bundled/iac_aliyun/references/template-parameters.md +206 -0
  89. iac_code/skills/bundled/iac_aliyun/references/terraform-template.md +101 -0
  90. iac_code/skills/bundled/iac_aliyun/scripts/tf2ros.py +77 -0
  91. iac_code/skills/bundled/simplify.py +28 -0
  92. iac_code/skills/discovery.py +136 -0
  93. iac_code/skills/frontmatter.py +119 -0
  94. iac_code/skills/listing.py +92 -0
  95. iac_code/skills/loader.py +42 -0
  96. iac_code/skills/processor.py +81 -0
  97. iac_code/skills/renderer.py +157 -0
  98. iac_code/skills/skill_definition.py +82 -0
  99. iac_code/skills/skill_tool.py +261 -0
  100. iac_code/state/__init__.py +5 -0
  101. iac_code/state/app_state.py +122 -0
  102. iac_code/tasks/__init__.py +1 -0
  103. iac_code/tasks/notification_queue.py +28 -0
  104. iac_code/tasks/task_state.py +66 -0
  105. iac_code/tasks/task_tools.py +114 -0
  106. iac_code/tools/__init__.py +8 -0
  107. iac_code/tools/base.py +226 -0
  108. iac_code/tools/bash.py +133 -0
  109. iac_code/tools/cloud/__init__.py +0 -0
  110. iac_code/tools/cloud/aliyun/__init__.py +0 -0
  111. iac_code/tools/cloud/aliyun/aliyun_api.py +510 -0
  112. iac_code/tools/cloud/aliyun/aliyun_doc_search.py +145 -0
  113. iac_code/tools/cloud/aliyun/endpoints.yml +343 -0
  114. iac_code/tools/cloud/aliyun/ros_client.py +56 -0
  115. iac_code/tools/cloud/aliyun/ros_stack.py +633 -0
  116. iac_code/tools/cloud/aliyun/ros_stack_instances.py +247 -0
  117. iac_code/tools/cloud/base_api.py +162 -0
  118. iac_code/tools/cloud/base_stack.py +242 -0
  119. iac_code/tools/cloud/registry.py +20 -0
  120. iac_code/tools/cloud/types.py +105 -0
  121. iac_code/tools/edit_file.py +121 -0
  122. iac_code/tools/glob.py +103 -0
  123. iac_code/tools/grep.py +254 -0
  124. iac_code/tools/list_files.py +104 -0
  125. iac_code/tools/read_file.py +127 -0
  126. iac_code/tools/result_storage.py +39 -0
  127. iac_code/tools/tool_executor.py +165 -0
  128. iac_code/tools/web_fetch.py +177 -0
  129. iac_code/tools/write_file.py +88 -0
  130. iac_code/types/__init__.py +40 -0
  131. iac_code/types/permissions.py +26 -0
  132. iac_code/types/skill_source.py +11 -0
  133. iac_code/types/stream_events.py +227 -0
  134. iac_code/ui/__init__.py +5 -0
  135. iac_code/ui/banner.py +110 -0
  136. iac_code/ui/components/__init__.py +0 -0
  137. iac_code/ui/components/dialog.py +142 -0
  138. iac_code/ui/components/divider.py +20 -0
  139. iac_code/ui/components/fuzzy_picker.py +308 -0
  140. iac_code/ui/components/progress_bar.py +54 -0
  141. iac_code/ui/components/search_box.py +165 -0
  142. iac_code/ui/components/select.py +319 -0
  143. iac_code/ui/components/status_icon.py +42 -0
  144. iac_code/ui/components/tabs.py +128 -0
  145. iac_code/ui/core/__init__.py +0 -0
  146. iac_code/ui/core/in_place_render.py +129 -0
  147. iac_code/ui/core/input_history.py +118 -0
  148. iac_code/ui/core/key_event.py +41 -0
  149. iac_code/ui/core/prompt_input.py +507 -0
  150. iac_code/ui/core/raw_input.py +302 -0
  151. iac_code/ui/core/screen.py +80 -0
  152. iac_code/ui/dialogs/__init__.py +0 -0
  153. iac_code/ui/dialogs/global_search.py +178 -0
  154. iac_code/ui/dialogs/history_search.py +100 -0
  155. iac_code/ui/dialogs/model_picker.py +280 -0
  156. iac_code/ui/dialogs/quick_open.py +108 -0
  157. iac_code/ui/dialogs/resume_picker.py +749 -0
  158. iac_code/ui/keybindings/__init__.py +0 -0
  159. iac_code/ui/keybindings/manager.py +124 -0
  160. iac_code/ui/renderer.py +1535 -0
  161. iac_code/ui/repl.py +772 -0
  162. iac_code/ui/spinner.py +112 -0
  163. iac_code/ui/suggestions/__init__.py +0 -0
  164. iac_code/ui/suggestions/aggregator.py +171 -0
  165. iac_code/ui/suggestions/command_provider.py +43 -0
  166. iac_code/ui/suggestions/directory_provider.py +95 -0
  167. iac_code/ui/suggestions/file_provider.py +121 -0
  168. iac_code/ui/suggestions/shell_history_provider.py +108 -0
  169. iac_code/ui/suggestions/token_extractor.py +77 -0
  170. iac_code/ui/suggestions/types.py +45 -0
  171. iac_code/ui/transcript_view.py +199 -0
  172. iac_code/utils/__init__.py +0 -0
  173. iac_code/utils/background_housekeeping.py +53 -0
  174. iac_code/utils/cleanup.py +68 -0
  175. iac_code/utils/json_utils.py +60 -0
  176. iac_code/utils/log.py +150 -0
  177. iac_code/utils/project_paths.py +74 -0
  178. iac_code/utils/tool_input_parser.py +62 -0
  179. iac_code-0.1.0.dist-info/LICENSE +201 -0
  180. iac_code-0.1.0.dist-info/METADATA +64 -0
  181. iac_code-0.1.0.dist-info/RECORD +184 -0
  182. iac_code-0.1.0.dist-info/WHEEL +5 -0
  183. iac_code-0.1.0.dist-info/entry_points.txt +2 -0
  184. iac_code-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,66 @@
1
+ """Background task state management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from collections import OrderedDict
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+
10
+
11
+ class TaskStatus(str, Enum):
12
+ RUNNING = "running"
13
+ COMPLETED = "completed"
14
+ FAILED = "failed"
15
+ STOPPED = "stopped"
16
+
17
+
18
+ @dataclass
19
+ class TaskInfo:
20
+ id: str
21
+ description: str
22
+ agent_type: str = "general-purpose"
23
+ status: TaskStatus = TaskStatus.RUNNING
24
+ result: str | None = None
25
+ error: str | None = None
26
+ tool_use_count: int = 0
27
+ token_count: int = 0
28
+
29
+
30
+ class TaskManager:
31
+ def __init__(self) -> None:
32
+ self._tasks: OrderedDict[str, TaskInfo] = OrderedDict()
33
+
34
+ def register(self, description: str, agent_type: str = "general-purpose") -> str:
35
+ task_id = str(uuid.uuid4())[:8]
36
+ self._tasks[task_id] = TaskInfo(id=task_id, description=description, agent_type=agent_type)
37
+ return task_id
38
+
39
+ def get(self, task_id: str) -> TaskInfo | None:
40
+ return self._tasks.get(task_id)
41
+
42
+ def complete(self, task_id: str, result: str) -> None:
43
+ task = self._tasks.get(task_id)
44
+ if task:
45
+ task.status = TaskStatus.COMPLETED
46
+ task.result = result
47
+
48
+ def fail(self, task_id: str, error: str) -> None:
49
+ task = self._tasks.get(task_id)
50
+ if task:
51
+ task.status = TaskStatus.FAILED
52
+ task.error = error
53
+
54
+ def stop(self, task_id: str) -> None:
55
+ task = self._tasks.get(task_id)
56
+ if task:
57
+ task.status = TaskStatus.STOPPED
58
+
59
+ def update_progress(self, task_id: str, tool_use_count: int = 0, token_count: int = 0) -> None:
60
+ task = self._tasks.get(task_id)
61
+ if task:
62
+ task.tool_use_count = tool_use_count
63
+ task.token_count = token_count
64
+
65
+ def list_all(self) -> list[TaskInfo]:
66
+ return list(self._tasks.values())
@@ -0,0 +1,114 @@
1
+ """Model-side task tools for managing background agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from iac_code.tasks.task_state import TaskManager
8
+ from iac_code.tools.base import Tool, ToolContext, ToolResult
9
+
10
+
11
+ class TaskListTool(Tool):
12
+ def __init__(self, task_manager: TaskManager):
13
+ self._manager = task_manager
14
+
15
+ @property
16
+ def name(self) -> str:
17
+ return "task_list"
18
+
19
+ @property
20
+ def description(self) -> str:
21
+ return "List all background tasks with their status."
22
+
23
+ @property
24
+ def input_schema(self) -> dict[str, Any]:
25
+ return {"type": "object", "properties": {}}
26
+
27
+ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
28
+ tasks = self._manager.list_all()
29
+ if not tasks:
30
+ return ToolResult.success("No background tasks.")
31
+ lines = []
32
+ for t in tasks:
33
+ lines.append(f"- [{t.id}] {t.status.value} | [{t.agent_type}] {t.description}")
34
+ if t.result:
35
+ lines.append(f" Result: {t.result[:200]}")
36
+ if t.error:
37
+ lines.append(f" Error: {t.error[:200]}")
38
+ return ToolResult.success("\n".join(lines))
39
+
40
+ def is_read_only(self, input: dict | None = None) -> bool:
41
+ return True
42
+
43
+
44
+ class TaskGetTool(Tool):
45
+ def __init__(self, task_manager: TaskManager):
46
+ self._manager = task_manager
47
+
48
+ @property
49
+ def name(self) -> str:
50
+ return "task_get"
51
+
52
+ @property
53
+ def description(self) -> str:
54
+ return "Get details of a specific background task by ID."
55
+
56
+ @property
57
+ def input_schema(self) -> dict[str, Any]:
58
+ return {
59
+ "type": "object",
60
+ "properties": {"task_id": {"type": "string"}},
61
+ "required": ["task_id"],
62
+ }
63
+
64
+ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
65
+ task = self._manager.get(tool_input["task_id"])
66
+ if not task:
67
+ return ToolResult.error(f"Task '{tool_input['task_id']}' not found.")
68
+ lines = [
69
+ f"ID: {task.id}",
70
+ f"Description: {task.description}",
71
+ f"Status: {task.status.value}",
72
+ f"Agent type: {task.agent_type}",
73
+ f"Tool uses: {task.tool_use_count}",
74
+ f"Tokens: {task.token_count}",
75
+ ]
76
+ if task.result:
77
+ lines.append(f"Result: {task.result}")
78
+ if task.error:
79
+ lines.append(f"Error: {task.error}")
80
+ return ToolResult.success("\n".join(lines))
81
+
82
+ def is_read_only(self, input: dict | None = None) -> bool:
83
+ return True
84
+
85
+
86
+ class TaskStopTool(Tool):
87
+ def __init__(self, task_manager: TaskManager):
88
+ self._manager = task_manager
89
+
90
+ @property
91
+ def name(self) -> str:
92
+ return "task_stop"
93
+
94
+ @property
95
+ def description(self) -> str:
96
+ return "Stop a running background task."
97
+
98
+ @property
99
+ def input_schema(self) -> dict[str, Any]:
100
+ return {
101
+ "type": "object",
102
+ "properties": {"task_id": {"type": "string"}},
103
+ "required": ["task_id"],
104
+ }
105
+
106
+ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
107
+ task = self._manager.get(tool_input["task_id"])
108
+ if not task:
109
+ return ToolResult.error(f"Task '{tool_input['task_id']}' not found.")
110
+ self._manager.stop(tool_input["task_id"])
111
+ return ToolResult.success(f"Task '{tool_input['task_id']}' stopped.")
112
+
113
+ def is_read_only(self, input: dict | None = None) -> bool:
114
+ return False
@@ -0,0 +1,8 @@
1
+ from iac_code.tools.base import Tool, ToolContext, ToolRegistry, ToolResult
2
+
3
+ __all__ = [
4
+ "Tool",
5
+ "ToolContext",
6
+ "ToolRegistry",
7
+ "ToolResult",
8
+ ]
iac_code/tools/base.py ADDED
@@ -0,0 +1,226 @@
1
+ """Base classes for the tool system."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ from abc import ABC, abstractmethod
8
+ from collections.abc import Callable
9
+ from dataclasses import dataclass, field
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ if TYPE_CHECKING:
13
+ from iac_code.types.permissions import PermissionResult
14
+
15
+
16
+ @dataclass
17
+ class ToolContext:
18
+ """Execution context passed to tools."""
19
+
20
+ cwd: str = field(default_factory=os.getcwd)
21
+ event_queue: asyncio.Queue | None = None
22
+
23
+
24
+ @dataclass
25
+ class ToolResult:
26
+ """Result returned from tool execution.
27
+
28
+ Extended to support context modification:
29
+ - new_messages: Additional messages to inject into conversation context.
30
+ - context_modifier: Callback to modify the execution context
31
+ (e.g., add tool permissions, override model, override effort).
32
+ """
33
+
34
+ content: str
35
+ is_error: bool = False
36
+ new_messages: list[dict[str, Any]] = field(default_factory=list)
37
+ context_modifier: Callable[[dict], dict] | None = None
38
+
39
+ @staticmethod
40
+ def error(message: str) -> ToolResult:
41
+ """Create an error result."""
42
+ return ToolResult(content=message, is_error=True)
43
+
44
+ @staticmethod
45
+ def success(content: str) -> ToolResult:
46
+ """Create a success result."""
47
+ return ToolResult(content=content, is_error=False)
48
+
49
+
50
+ class Tool(ABC):
51
+ """Abstract base class for all tools."""
52
+
53
+ @property
54
+ @abstractmethod
55
+ def name(self) -> str:
56
+ """The unique name of the tool."""
57
+ ...
58
+
59
+ @property
60
+ @abstractmethod
61
+ def description(self) -> str:
62
+ """A description of what the tool does."""
63
+ ...
64
+
65
+ @property
66
+ @abstractmethod
67
+ def input_schema(self) -> dict[str, Any]:
68
+ """JSON Schema defining the tool's input parameters."""
69
+ ...
70
+
71
+ @abstractmethod
72
+ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
73
+ """Execute the tool with the given input.
74
+
75
+ Args:
76
+ tool_input: The input parameters as defined by input_schema.
77
+ context: The execution context.
78
+
79
+ Returns:
80
+ The result of the tool execution.
81
+ """
82
+ ...
83
+
84
+ def normalize_input(self, tool_input: dict[str, Any]) -> None:
85
+ """Normalize tool input in-place before validation.
86
+
87
+ Override in subclasses to handle parameter aliases.
88
+ """
89
+
90
+ def validate_input(self, tool_input: dict[str, Any]) -> tuple[bool, str]:
91
+ """Validate tool_input against input_schema using JSON Schema.
92
+
93
+ Returns:
94
+ (True, "") if valid, (False, error_message) if invalid.
95
+ """
96
+ self.normalize_input(tool_input)
97
+ try:
98
+ import jsonschema
99
+
100
+ jsonschema.validate(instance=tool_input, schema=self.input_schema)
101
+ return True, ""
102
+ except jsonschema.ValidationError as e:
103
+ return False, str(e.message)
104
+
105
+ def to_api_format(self) -> dict[str, Any]:
106
+ """Convert tool definition to LLM API format (legacy OpenAI format)."""
107
+ return {
108
+ "type": "function",
109
+ "function": {
110
+ "name": self.name,
111
+ "description": self.description,
112
+ "parameters": self.input_schema,
113
+ },
114
+ }
115
+
116
+ # --- UI rendering methods ---
117
+ def render_tool_use_message(self, input: dict, *, verbose: bool = False) -> str | None:
118
+ """Return detail text shown in parentheses after the tool name.
119
+
120
+ Displayed as ``● ToolName(detail)`` in the UI. Use this to show
121
+ the key parameters of the tool invocation, e.g. file path, search
122
+ pattern, or URL.
123
+ """
124
+ return None
125
+
126
+ def render_tool_result_message(self, output: str, *, is_error: bool = False, verbose: bool = False) -> str | None:
127
+ """Return result summary shown after ``⎿`` prefix.
128
+
129
+ In non-verbose mode, return a short one-line summary (e.g.
130
+ "Found 5 files"). In verbose mode, include more detail.
131
+ """
132
+ return None
133
+
134
+ def render_tool_use_error_message(self, error: str) -> str | None:
135
+ """Return error text for display."""
136
+ return None
137
+
138
+ # --- Display info ---
139
+ def user_facing_name(self, input: dict | None = None) -> str:
140
+ """Short, human-readable tool name shown as the bold label in the UI.
141
+
142
+ Should be a concise noun like "Read", "Search", or "Bash".
143
+ Do NOT include parameter details here — use
144
+ ``render_tool_use_message`` for that. This name is also used in
145
+ permission prompts (e.g. "Allow Read?").
146
+ """
147
+ return self.name
148
+
149
+ def get_activity_description(self, input: dict | None = None) -> str | None:
150
+ """Text shown in the spinner while the tool is executing."""
151
+ return None
152
+
153
+ def get_tool_use_summary(self, input: dict | None = None) -> str | None:
154
+ """Optional one-line summary of the tool invocation."""
155
+ return None
156
+
157
+ # --- Permission methods ---
158
+ @property
159
+ def timeout(self) -> float | None:
160
+ """Per-tool timeout in seconds. None means use the global default."""
161
+ return None
162
+
163
+ def is_read_only(self, input: dict | None = None) -> bool:
164
+ """Whether the tool only reads and never modifies state."""
165
+ return False
166
+
167
+ def is_concurrency_safe(self, tool_input: dict[str, Any]) -> bool:
168
+ """Whether this tool can safely run concurrently with other tools.
169
+ By default, read-only tools are concurrency safe.
170
+ """
171
+ return self.is_read_only(tool_input)
172
+
173
+ def is_destructive(self, input: dict | None = None) -> bool:
174
+ """Whether the tool performs destructive operations."""
175
+ return False
176
+
177
+ async def check_permissions(self, input: dict, context: dict | None = None) -> "PermissionResult":
178
+ """Check permissions"""
179
+ from iac_code.types.permissions import PermissionResult
180
+
181
+ if self.is_read_only(input):
182
+ return PermissionResult(behavior="allow")
183
+ return PermissionResult(behavior="ask", message=f"Allow {self.user_facing_name(input)}?")
184
+
185
+
186
+ class ToolRegistry:
187
+ """Registry for managing available tools."""
188
+
189
+ def __init__(self) -> None:
190
+ self._tools: dict[str, Tool] = {}
191
+
192
+ def register(self, tool: Tool) -> None:
193
+ """Register a tool."""
194
+ self._tools[tool.name] = tool
195
+
196
+ def get(self, name: str) -> Tool | None:
197
+ """Get a tool by name."""
198
+ return self._tools.get(name)
199
+
200
+ def list_tools(self) -> list[Tool]:
201
+ """List all registered tools."""
202
+ return list(self._tools.values())
203
+
204
+ def to_api_format(self) -> list[dict[str, Any]]:
205
+ """Convert all tools to LLM API format (legacy OpenAI format)."""
206
+ return [tool.to_api_format() for tool in self._tools.values()]
207
+
208
+ def register_default_tools(self) -> None:
209
+ """Register all default built-in tools."""
210
+ from iac_code.tools.bash import BashTool
211
+ from iac_code.tools.edit_file import EditFileTool
212
+ from iac_code.tools.glob import GlobTool
213
+ from iac_code.tools.grep import GrepTool
214
+ from iac_code.tools.list_files import ListFilesTool
215
+ from iac_code.tools.read_file import ReadFileTool
216
+ from iac_code.tools.web_fetch import WebFetchTool
217
+ from iac_code.tools.write_file import WriteFileTool
218
+
219
+ self.register(ReadFileTool())
220
+ self.register(WriteFileTool())
221
+ self.register(EditFileTool())
222
+ self.register(BashTool())
223
+ self.register(ListFilesTool())
224
+ self.register(GlobTool())
225
+ self.register(GrepTool())
226
+ self.register(WebFetchTool())
iac_code/tools/bash.py ADDED
@@ -0,0 +1,133 @@
1
+ """Bash tool - executes shell commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ from iac_code.i18n import _
9
+ from iac_code.tools.base import Tool, ToolContext, ToolResult
10
+
11
+
12
+ class BashTool(Tool):
13
+ @property
14
+ def name(self) -> str:
15
+ return "bash"
16
+
17
+ @property
18
+ def description(self) -> str:
19
+ return (
20
+ "Execute a shell command in the system's default shell. "
21
+ "Use this for running programs, installing packages, searching code, "
22
+ "running tests, git operations, and other system tasks. "
23
+ "Commands are executed with a timeout of 120 seconds by default."
24
+ )
25
+
26
+ @property
27
+ def input_schema(self) -> dict[str, Any]:
28
+ return {
29
+ "type": "object",
30
+ "properties": {
31
+ "command": {
32
+ "type": "string",
33
+ "description": "The shell command to execute.",
34
+ },
35
+ "timeout": {
36
+ "type": "integer",
37
+ "description": "Timeout in seconds. Defaults to 120.",
38
+ },
39
+ },
40
+ "required": ["command"],
41
+ }
42
+
43
+ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
44
+ command = tool_input["command"]
45
+ timeout = tool_input.get("timeout", 120)
46
+
47
+ try:
48
+ process = await asyncio.create_subprocess_shell(
49
+ command,
50
+ stdout=asyncio.subprocess.PIPE,
51
+ stderr=asyncio.subprocess.PIPE,
52
+ cwd=context.cwd,
53
+ )
54
+
55
+ try:
56
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
57
+ except asyncio.TimeoutError:
58
+ process.kill()
59
+ await process.communicate()
60
+ return ToolResult.error(f"Command timed out after {timeout} seconds: {command}")
61
+
62
+ stdout_str = stdout.decode("utf-8", errors="replace") if stdout else ""
63
+ stderr_str = stderr.decode("utf-8", errors="replace") if stderr else ""
64
+
65
+ # Build result
66
+ parts = []
67
+ if stdout_str:
68
+ parts.append(f"STDOUT:\n{stdout_str}")
69
+ if stderr_str:
70
+ parts.append(f"STDERR:\n{stderr_str}")
71
+ parts.append(f"Exit code: {process.returncode}")
72
+
73
+ output = "\n".join(parts)
74
+
75
+ if process.returncode != 0:
76
+ return ToolResult.error(output)
77
+
78
+ return ToolResult.success(output)
79
+
80
+ except Exception as e:
81
+ return ToolResult.error(f"Error executing command: {e}")
82
+
83
+ # UI rendering methods
84
+ MAX_COMMAND_DISPLAY_CHARS = 160
85
+ MAX_COMMAND_DISPLAY_LINES = 2
86
+ MAX_OUTPUT_LINES = 20
87
+
88
+ def render_tool_use_message(self, input: dict, *, verbose: bool = False):
89
+ cmd = input.get("command", "")
90
+ if not cmd:
91
+ return None
92
+ if not verbose:
93
+ lines = cmd.split("\n")
94
+ if len(lines) > self.MAX_COMMAND_DISPLAY_LINES:
95
+ cmd = "\n".join(lines[: self.MAX_COMMAND_DISPLAY_LINES])
96
+ if len(cmd) > self.MAX_COMMAND_DISPLAY_CHARS:
97
+ cmd = cmd[: self.MAX_COMMAND_DISPLAY_CHARS]
98
+ return cmd.strip() + "…" if cmd != input.get("command", "") else cmd
99
+ return cmd
100
+
101
+ def render_tool_result_message(self, output: str, *, is_error: bool = False, verbose: bool = False):
102
+ lines = output.strip().splitlines()
103
+ if not verbose and len(lines) > self.MAX_OUTPUT_LINES:
104
+ return (
105
+ "\n".join(lines[: self.MAX_OUTPUT_LINES])
106
+ + "\n... "
107
+ + _("{count} more lines").format(count=len(lines) - self.MAX_OUTPUT_LINES)
108
+ )
109
+ return output.strip()
110
+
111
+ def render_tool_use_error_message(self, error: str):
112
+ return error
113
+
114
+ def user_facing_name(self, input: dict | None = None) -> str:
115
+ return _("Bash")
116
+
117
+ def get_activity_description(self, input: dict | None = None) -> str:
118
+ if input:
119
+ cmd = input.get("command", "")
120
+ short = cmd[:50] + "..." if len(cmd) > 50 else cmd
121
+ return _("Running {cmd}").format(cmd=short)
122
+ return _("Running command...")
123
+
124
+ def get_tool_use_summary(self, input: dict | None = None) -> str | None:
125
+ if input:
126
+ return input.get("command", "")[:80]
127
+ return None
128
+
129
+ def is_read_only(self, input: dict | None = None) -> bool:
130
+ return False
131
+
132
+ def is_destructive(self, input: dict | None = None) -> bool:
133
+ return False
File without changes
File without changes