llmcode-cli 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.
Files changed (212) hide show
  1. llm_code/__init__.py +2 -0
  2. llm_code/analysis/__init__.py +6 -0
  3. llm_code/analysis/cache.py +33 -0
  4. llm_code/analysis/engine.py +256 -0
  5. llm_code/analysis/go_rules.py +114 -0
  6. llm_code/analysis/js_rules.py +84 -0
  7. llm_code/analysis/python_rules.py +311 -0
  8. llm_code/analysis/rules.py +140 -0
  9. llm_code/analysis/rust_rules.py +108 -0
  10. llm_code/analysis/universal_rules.py +111 -0
  11. llm_code/api/__init__.py +0 -0
  12. llm_code/api/client.py +90 -0
  13. llm_code/api/errors.py +73 -0
  14. llm_code/api/openai_compat.py +390 -0
  15. llm_code/api/provider.py +35 -0
  16. llm_code/api/sse.py +52 -0
  17. llm_code/api/types.py +140 -0
  18. llm_code/cli/__init__.py +0 -0
  19. llm_code/cli/commands.py +70 -0
  20. llm_code/cli/image.py +122 -0
  21. llm_code/cli/render.py +214 -0
  22. llm_code/cli/status_line.py +79 -0
  23. llm_code/cli/streaming.py +92 -0
  24. llm_code/cli/tui_main.py +220 -0
  25. llm_code/computer_use/__init__.py +11 -0
  26. llm_code/computer_use/app_detect.py +49 -0
  27. llm_code/computer_use/app_tier.py +57 -0
  28. llm_code/computer_use/coordinator.py +99 -0
  29. llm_code/computer_use/input_control.py +71 -0
  30. llm_code/computer_use/screenshot.py +93 -0
  31. llm_code/cron/__init__.py +13 -0
  32. llm_code/cron/parser.py +145 -0
  33. llm_code/cron/scheduler.py +135 -0
  34. llm_code/cron/storage.py +126 -0
  35. llm_code/enterprise/__init__.py +1 -0
  36. llm_code/enterprise/audit.py +59 -0
  37. llm_code/enterprise/auth.py +26 -0
  38. llm_code/enterprise/oidc.py +95 -0
  39. llm_code/enterprise/rbac.py +65 -0
  40. llm_code/harness/__init__.py +5 -0
  41. llm_code/harness/config.py +33 -0
  42. llm_code/harness/engine.py +129 -0
  43. llm_code/harness/guides.py +41 -0
  44. llm_code/harness/sensors.py +68 -0
  45. llm_code/harness/templates.py +84 -0
  46. llm_code/hida/__init__.py +1 -0
  47. llm_code/hida/classifier.py +187 -0
  48. llm_code/hida/engine.py +49 -0
  49. llm_code/hida/profiles.py +95 -0
  50. llm_code/hida/types.py +28 -0
  51. llm_code/ide/__init__.py +1 -0
  52. llm_code/ide/bridge.py +80 -0
  53. llm_code/ide/detector.py +76 -0
  54. llm_code/ide/server.py +169 -0
  55. llm_code/logging.py +29 -0
  56. llm_code/lsp/__init__.py +0 -0
  57. llm_code/lsp/client.py +298 -0
  58. llm_code/lsp/detector.py +42 -0
  59. llm_code/lsp/manager.py +56 -0
  60. llm_code/lsp/tools.py +288 -0
  61. llm_code/marketplace/__init__.py +0 -0
  62. llm_code/marketplace/builtin_registry.py +102 -0
  63. llm_code/marketplace/installer.py +162 -0
  64. llm_code/marketplace/plugin.py +78 -0
  65. llm_code/marketplace/registry.py +360 -0
  66. llm_code/mcp/__init__.py +0 -0
  67. llm_code/mcp/bridge.py +87 -0
  68. llm_code/mcp/client.py +117 -0
  69. llm_code/mcp/health.py +120 -0
  70. llm_code/mcp/manager.py +214 -0
  71. llm_code/mcp/oauth.py +219 -0
  72. llm_code/mcp/transport.py +254 -0
  73. llm_code/mcp/types.py +53 -0
  74. llm_code/remote/__init__.py +0 -0
  75. llm_code/remote/client.py +136 -0
  76. llm_code/remote/protocol.py +22 -0
  77. llm_code/remote/server.py +275 -0
  78. llm_code/remote/ssh_proxy.py +56 -0
  79. llm_code/runtime/__init__.py +0 -0
  80. llm_code/runtime/auto_commit.py +56 -0
  81. llm_code/runtime/auto_diagnose.py +62 -0
  82. llm_code/runtime/checkpoint.py +70 -0
  83. llm_code/runtime/checkpoint_recovery.py +142 -0
  84. llm_code/runtime/compaction.py +35 -0
  85. llm_code/runtime/compressor.py +415 -0
  86. llm_code/runtime/config.py +533 -0
  87. llm_code/runtime/context.py +49 -0
  88. llm_code/runtime/conversation.py +921 -0
  89. llm_code/runtime/cost_tracker.py +126 -0
  90. llm_code/runtime/dream.py +127 -0
  91. llm_code/runtime/file_protection.py +150 -0
  92. llm_code/runtime/hardware.py +85 -0
  93. llm_code/runtime/hooks.py +223 -0
  94. llm_code/runtime/indexer.py +230 -0
  95. llm_code/runtime/knowledge_compiler.py +232 -0
  96. llm_code/runtime/memory.py +132 -0
  97. llm_code/runtime/memory_layers.py +467 -0
  98. llm_code/runtime/memory_lint.py +252 -0
  99. llm_code/runtime/model_aliases.py +37 -0
  100. llm_code/runtime/ollama.py +93 -0
  101. llm_code/runtime/overlay.py +124 -0
  102. llm_code/runtime/permissions.py +200 -0
  103. llm_code/runtime/plan.py +45 -0
  104. llm_code/runtime/prompt.py +238 -0
  105. llm_code/runtime/repo_map.py +174 -0
  106. llm_code/runtime/sandbox.py +116 -0
  107. llm_code/runtime/session.py +268 -0
  108. llm_code/runtime/skill_resolver.py +61 -0
  109. llm_code/runtime/skills.py +133 -0
  110. llm_code/runtime/speculative.py +75 -0
  111. llm_code/runtime/streaming_executor.py +216 -0
  112. llm_code/runtime/telemetry.py +196 -0
  113. llm_code/runtime/token_budget.py +26 -0
  114. llm_code/runtime/vcr.py +142 -0
  115. llm_code/runtime/vision.py +102 -0
  116. llm_code/swarm/__init__.py +1 -0
  117. llm_code/swarm/backend_subprocess.py +108 -0
  118. llm_code/swarm/backend_tmux.py +103 -0
  119. llm_code/swarm/backend_worktree.py +306 -0
  120. llm_code/swarm/checkpoint.py +74 -0
  121. llm_code/swarm/coordinator.py +236 -0
  122. llm_code/swarm/mailbox.py +88 -0
  123. llm_code/swarm/manager.py +202 -0
  124. llm_code/swarm/memory_sync.py +80 -0
  125. llm_code/swarm/recovery.py +21 -0
  126. llm_code/swarm/team.py +67 -0
  127. llm_code/swarm/types.py +31 -0
  128. llm_code/task/__init__.py +16 -0
  129. llm_code/task/diagnostics.py +93 -0
  130. llm_code/task/manager.py +162 -0
  131. llm_code/task/types.py +112 -0
  132. llm_code/task/verifier.py +104 -0
  133. llm_code/tools/__init__.py +0 -0
  134. llm_code/tools/agent.py +145 -0
  135. llm_code/tools/agent_roles.py +82 -0
  136. llm_code/tools/base.py +94 -0
  137. llm_code/tools/bash.py +565 -0
  138. llm_code/tools/computer_use_tools.py +278 -0
  139. llm_code/tools/coordinator_tool.py +75 -0
  140. llm_code/tools/cron_create.py +90 -0
  141. llm_code/tools/cron_delete.py +49 -0
  142. llm_code/tools/cron_list.py +51 -0
  143. llm_code/tools/deferred.py +92 -0
  144. llm_code/tools/dump.py +116 -0
  145. llm_code/tools/edit_file.py +282 -0
  146. llm_code/tools/git_tools.py +531 -0
  147. llm_code/tools/glob_search.py +112 -0
  148. llm_code/tools/grep_search.py +144 -0
  149. llm_code/tools/ide_diagnostics.py +59 -0
  150. llm_code/tools/ide_open.py +58 -0
  151. llm_code/tools/ide_selection.py +52 -0
  152. llm_code/tools/memory_tools.py +138 -0
  153. llm_code/tools/multi_edit.py +143 -0
  154. llm_code/tools/notebook_edit.py +107 -0
  155. llm_code/tools/notebook_read.py +81 -0
  156. llm_code/tools/parsing.py +63 -0
  157. llm_code/tools/read_file.py +154 -0
  158. llm_code/tools/registry.py +58 -0
  159. llm_code/tools/search_backends/__init__.py +56 -0
  160. llm_code/tools/search_backends/brave.py +56 -0
  161. llm_code/tools/search_backends/duckduckgo.py +129 -0
  162. llm_code/tools/search_backends/searxng.py +71 -0
  163. llm_code/tools/search_backends/tavily.py +73 -0
  164. llm_code/tools/swarm_create.py +109 -0
  165. llm_code/tools/swarm_delete.py +95 -0
  166. llm_code/tools/swarm_list.py +44 -0
  167. llm_code/tools/swarm_message.py +109 -0
  168. llm_code/tools/task_close.py +79 -0
  169. llm_code/tools/task_plan.py +79 -0
  170. llm_code/tools/task_verify.py +90 -0
  171. llm_code/tools/tool_search.py +65 -0
  172. llm_code/tools/web_common.py +258 -0
  173. llm_code/tools/web_fetch.py +223 -0
  174. llm_code/tools/web_search.py +280 -0
  175. llm_code/tools/write_file.py +118 -0
  176. llm_code/tui/__init__.py +1 -0
  177. llm_code/tui/app.py +2432 -0
  178. llm_code/tui/chat_view.py +82 -0
  179. llm_code/tui/chat_widgets.py +309 -0
  180. llm_code/tui/header_bar.py +46 -0
  181. llm_code/tui/input_bar.py +349 -0
  182. llm_code/tui/keybindings.py +142 -0
  183. llm_code/tui/marketplace.py +210 -0
  184. llm_code/tui/status_bar.py +72 -0
  185. llm_code/tui/theme.py +96 -0
  186. llm_code/utils/__init__.py +0 -0
  187. llm_code/utils/diff.py +111 -0
  188. llm_code/utils/errors.py +70 -0
  189. llm_code/utils/hyperlink.py +73 -0
  190. llm_code/utils/notebook.py +179 -0
  191. llm_code/utils/search.py +69 -0
  192. llm_code/utils/text_normalize.py +28 -0
  193. llm_code/utils/version_check.py +62 -0
  194. llm_code/vim/__init__.py +4 -0
  195. llm_code/vim/engine.py +51 -0
  196. llm_code/vim/motions.py +172 -0
  197. llm_code/vim/operators.py +183 -0
  198. llm_code/vim/text_objects.py +139 -0
  199. llm_code/vim/transitions.py +279 -0
  200. llm_code/vim/types.py +68 -0
  201. llm_code/voice/__init__.py +1 -0
  202. llm_code/voice/languages.py +43 -0
  203. llm_code/voice/recorder.py +136 -0
  204. llm_code/voice/stt.py +36 -0
  205. llm_code/voice/stt_anthropic.py +66 -0
  206. llm_code/voice/stt_google.py +32 -0
  207. llm_code/voice/stt_whisper.py +52 -0
  208. llmcode_cli-1.0.0.dist-info/METADATA +524 -0
  209. llmcode_cli-1.0.0.dist-info/RECORD +212 -0
  210. llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
  211. llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
  212. llmcode_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,278 @@
1
+ """Tool classes for computer use (GUI automation)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import TYPE_CHECKING
6
+
7
+ from llm_code.tools.base import PermissionLevel, Tool, ToolResult
8
+
9
+ if TYPE_CHECKING:
10
+ from llm_code.runtime.config import ComputerUseConfig
11
+
12
+
13
+ class _ComputerUseTool(Tool):
14
+ """Base class for computer-use tools with shared config check."""
15
+
16
+ def __init__(self, config: "ComputerUseConfig") -> None:
17
+ from llm_code.computer_use.coordinator import ComputerUseCoordinator
18
+ self._config = config
19
+ self._coordinator = ComputerUseCoordinator(config)
20
+
21
+ def _check_enabled(self) -> ToolResult | None:
22
+ if not self._config.enabled:
23
+ return ToolResult(
24
+ output="Computer use is not enabled. Set computer_use.enabled=true in config.",
25
+ is_error=True,
26
+ )
27
+ return None
28
+
29
+
30
+ class ScreenshotTool(_ComputerUseTool):
31
+ @property
32
+ def name(self) -> str:
33
+ return "screenshot"
34
+
35
+ @property
36
+ def description(self) -> str:
37
+ return "Take a screenshot of the current screen. Returns a base64-encoded PNG image."
38
+
39
+ @property
40
+ def input_schema(self) -> dict:
41
+ return {
42
+ "type": "object",
43
+ "properties": {},
44
+ "additionalProperties": False,
45
+ }
46
+
47
+ @property
48
+ def required_permission(self) -> PermissionLevel:
49
+ return PermissionLevel.READ_ONLY
50
+
51
+ def is_read_only(self, args: dict) -> bool:
52
+ return True
53
+
54
+ def execute(self, args: dict) -> ToolResult:
55
+ err = self._check_enabled()
56
+ if err:
57
+ return err
58
+ try:
59
+ result = self._coordinator.screenshot()
60
+ return ToolResult(
61
+ output=json.dumps(result),
62
+ metadata={"has_image": True},
63
+ )
64
+ except Exception as exc:
65
+ return ToolResult(output=f"Screenshot failed: {exc}", is_error=True)
66
+
67
+
68
+ class MouseClickTool(_ComputerUseTool):
69
+ @property
70
+ def name(self) -> str:
71
+ return "mouse_click"
72
+
73
+ @property
74
+ def description(self) -> str:
75
+ return "Click the mouse at (x, y) coordinates. Returns a screenshot after clicking."
76
+
77
+ @property
78
+ def input_schema(self) -> dict:
79
+ return {
80
+ "type": "object",
81
+ "properties": {
82
+ "x": {"type": "integer", "description": "X coordinate"},
83
+ "y": {"type": "integer", "description": "Y coordinate"},
84
+ "button": {
85
+ "type": "string",
86
+ "enum": ["left", "right", "middle"],
87
+ "default": "left",
88
+ "description": "Mouse button",
89
+ },
90
+ },
91
+ "required": ["x", "y"],
92
+ "additionalProperties": False,
93
+ }
94
+
95
+ @property
96
+ def required_permission(self) -> PermissionLevel:
97
+ return PermissionLevel.FULL_ACCESS
98
+
99
+ def execute(self, args: dict) -> ToolResult:
100
+ err = self._check_enabled()
101
+ if err:
102
+ return err
103
+ try:
104
+ result = self._coordinator.click_and_observe(
105
+ x=args["x"],
106
+ y=args["y"],
107
+ button=args.get("button", "left"),
108
+ )
109
+ return ToolResult(output=json.dumps(result), metadata={"has_image": True})
110
+ except Exception as exc:
111
+ return ToolResult(output=f"Mouse click failed: {exc}", is_error=True)
112
+
113
+
114
+ class KeyboardTypeTool(_ComputerUseTool):
115
+ @property
116
+ def name(self) -> str:
117
+ return "keyboard_type"
118
+
119
+ @property
120
+ def description(self) -> str:
121
+ return "Type text using the keyboard. Returns a screenshot after typing."
122
+
123
+ @property
124
+ def input_schema(self) -> dict:
125
+ return {
126
+ "type": "object",
127
+ "properties": {
128
+ "text": {"type": "string", "description": "Text to type"},
129
+ },
130
+ "required": ["text"],
131
+ "additionalProperties": False,
132
+ }
133
+
134
+ @property
135
+ def required_permission(self) -> PermissionLevel:
136
+ return PermissionLevel.FULL_ACCESS
137
+
138
+ def execute(self, args: dict) -> ToolResult:
139
+ err = self._check_enabled()
140
+ if err:
141
+ return err
142
+ try:
143
+ result = self._coordinator.type_and_observe(args["text"])
144
+ return ToolResult(output=json.dumps(result), metadata={"has_image": True})
145
+ except Exception as exc:
146
+ return ToolResult(output=f"Keyboard type failed: {exc}", is_error=True)
147
+
148
+
149
+ class KeyPressTool(_ComputerUseTool):
150
+ @property
151
+ def name(self) -> str:
152
+ return "key_press"
153
+
154
+ @property
155
+ def description(self) -> str:
156
+ return "Press a keyboard shortcut (e.g., ctrl+c). Returns a screenshot after pressing."
157
+
158
+ @property
159
+ def input_schema(self) -> dict:
160
+ return {
161
+ "type": "object",
162
+ "properties": {
163
+ "keys": {
164
+ "type": "array",
165
+ "items": {"type": "string"},
166
+ "description": "Keys to press simultaneously (e.g., ['ctrl', 'c'])",
167
+ },
168
+ },
169
+ "required": ["keys"],
170
+ "additionalProperties": False,
171
+ }
172
+
173
+ @property
174
+ def required_permission(self) -> PermissionLevel:
175
+ return PermissionLevel.FULL_ACCESS
176
+
177
+ def execute(self, args: dict) -> ToolResult:
178
+ err = self._check_enabled()
179
+ if err:
180
+ return err
181
+ try:
182
+ result = self._coordinator.hotkey_and_observe(*args["keys"])
183
+ return ToolResult(output=json.dumps(result), metadata={"has_image": True})
184
+ except Exception as exc:
185
+ return ToolResult(output=f"Key press failed: {exc}", is_error=True)
186
+
187
+
188
+ class ScrollTool(_ComputerUseTool):
189
+ @property
190
+ def name(self) -> str:
191
+ return "scroll"
192
+
193
+ @property
194
+ def description(self) -> str:
195
+ return "Scroll the mouse wheel. Positive clicks = up, negative = down. Returns a screenshot."
196
+
197
+ @property
198
+ def input_schema(self) -> dict:
199
+ return {
200
+ "type": "object",
201
+ "properties": {
202
+ "clicks": {
203
+ "type": "integer",
204
+ "description": "Scroll amount (positive=up, negative=down)",
205
+ },
206
+ "x": {"type": "integer", "description": "Optional X position"},
207
+ "y": {"type": "integer", "description": "Optional Y position"},
208
+ },
209
+ "required": ["clicks"],
210
+ "additionalProperties": False,
211
+ }
212
+
213
+ @property
214
+ def required_permission(self) -> PermissionLevel:
215
+ return PermissionLevel.FULL_ACCESS
216
+
217
+ def execute(self, args: dict) -> ToolResult:
218
+ err = self._check_enabled()
219
+ if err:
220
+ return err
221
+ try:
222
+ result = self._coordinator.scroll_and_observe(
223
+ clicks=args["clicks"],
224
+ x=args.get("x"),
225
+ y=args.get("y"),
226
+ )
227
+ return ToolResult(output=json.dumps(result), metadata={"has_image": True})
228
+ except Exception as exc:
229
+ return ToolResult(output=f"Scroll failed: {exc}", is_error=True)
230
+
231
+
232
+ class MouseDragTool(_ComputerUseTool):
233
+ @property
234
+ def name(self) -> str:
235
+ return "mouse_drag"
236
+
237
+ @property
238
+ def description(self) -> str:
239
+ return "Drag the mouse from a start position by an offset. Returns a screenshot."
240
+
241
+ @property
242
+ def input_schema(self) -> dict:
243
+ return {
244
+ "type": "object",
245
+ "properties": {
246
+ "start_x": {"type": "integer", "description": "Start X coordinate"},
247
+ "start_y": {"type": "integer", "description": "Start Y coordinate"},
248
+ "offset_x": {"type": "integer", "description": "Horizontal drag distance"},
249
+ "offset_y": {"type": "integer", "description": "Vertical drag distance"},
250
+ "duration": {
251
+ "type": "number",
252
+ "default": 0.5,
253
+ "description": "Drag duration in seconds",
254
+ },
255
+ },
256
+ "required": ["start_x", "start_y", "offset_x", "offset_y"],
257
+ "additionalProperties": False,
258
+ }
259
+
260
+ @property
261
+ def required_permission(self) -> PermissionLevel:
262
+ return PermissionLevel.FULL_ACCESS
263
+
264
+ def execute(self, args: dict) -> ToolResult:
265
+ err = self._check_enabled()
266
+ if err:
267
+ return err
268
+ try:
269
+ result = self._coordinator.drag_and_observe(
270
+ start_x=args["start_x"],
271
+ start_y=args["start_y"],
272
+ offset_x=args["offset_x"],
273
+ offset_y=args["offset_y"],
274
+ duration=args.get("duration", 0.5),
275
+ )
276
+ return ToolResult(output=json.dumps(result), metadata={"has_image": True})
277
+ except Exception as exc:
278
+ return ToolResult(output=f"Mouse drag failed: {exc}", is_error=True)
@@ -0,0 +1,75 @@
1
+ """CoordinatorTool — auto-decompose and delegate a task to swarm workers."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import concurrent.futures
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from llm_code.tools.base import PermissionLevel, Tool, ToolResult
10
+
11
+
12
+ class CoordinatorInput(BaseModel):
13
+ task: str
14
+
15
+
16
+ class CoordinatorTool(Tool):
17
+ """Tool that auto-decomposes a high-level task and dispatches to swarm workers."""
18
+
19
+ def __init__(self, coordinator: object) -> None:
20
+ self._coordinator = coordinator
21
+
22
+ @property
23
+ def name(self) -> str:
24
+ return "coordinate"
25
+
26
+ @property
27
+ def description(self) -> str:
28
+ return (
29
+ "Auto-decompose a high-level task into subtasks and delegate each one "
30
+ "to a separate swarm worker agent. The coordinator monitors progress "
31
+ "and returns an aggregated summary when all workers finish (or timeout)."
32
+ )
33
+
34
+ @property
35
+ def input_schema(self) -> dict:
36
+ return {
37
+ "type": "object",
38
+ "properties": {
39
+ "task": {
40
+ "type": "string",
41
+ "description": "High-level task to decompose and delegate to worker agents.",
42
+ },
43
+ },
44
+ "required": ["task"],
45
+ }
46
+
47
+ @property
48
+ def required_permission(self) -> PermissionLevel:
49
+ return PermissionLevel.FULL_ACCESS
50
+
51
+ @property
52
+ def input_model(self) -> type[CoordinatorInput]:
53
+ return CoordinatorInput
54
+
55
+ def execute(self, args: dict) -> ToolResult:
56
+ task = args["task"]
57
+ try:
58
+ try:
59
+ asyncio.get_running_loop()
60
+ running = True
61
+ except RuntimeError:
62
+ running = False
63
+
64
+ if running:
65
+ with concurrent.futures.ThreadPoolExecutor() as pool:
66
+ result = pool.submit(
67
+ asyncio.run,
68
+ self._coordinator.orchestrate(task),
69
+ ).result()
70
+ else:
71
+ result = asyncio.run(self._coordinator.orchestrate(task))
72
+
73
+ return ToolResult(output=result)
74
+ except Exception as exc:
75
+ return ToolResult(output=f"Coordinator error: {exc}", is_error=True)
@@ -0,0 +1,90 @@
1
+ """CronCreateTool — schedule a new cron task."""
2
+ from __future__ import annotations
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from llm_code.cron.parser import parse_cron
7
+ from llm_code.cron.storage import CronStorage
8
+ from llm_code.tools.base import PermissionLevel, Tool, ToolResult
9
+
10
+
11
+ class CronCreateInput(BaseModel):
12
+ cron: str
13
+ prompt: str
14
+ recurring: bool = True
15
+ permanent: bool = False
16
+
17
+
18
+ class CronCreateTool(Tool):
19
+ def __init__(self, storage: CronStorage) -> None:
20
+ self._storage = storage
21
+
22
+ @property
23
+ def name(self) -> str:
24
+ return "cron_create"
25
+
26
+ @property
27
+ def description(self) -> str:
28
+ return (
29
+ "Schedule a prompt to run on a cron schedule. "
30
+ "5-field format: minute hour day-of-month month day-of-week (local time). "
31
+ "recurring=True keeps firing; permanent=True prevents 30-day auto-expiry."
32
+ )
33
+
34
+ @property
35
+ def input_schema(self) -> dict:
36
+ return {
37
+ "type": "object",
38
+ "properties": {
39
+ "cron": {"type": "string", "description": "Cron expression (5-field)"},
40
+ "prompt": {"type": "string", "description": "Prompt to execute when fired"},
41
+ "recurring": {"type": "boolean", "description": "Keep firing (default true)", "default": True},
42
+ "permanent": {"type": "boolean", "description": "Prevent 30-day auto-expiry (default false)", "default": False},
43
+ },
44
+ "required": ["cron", "prompt"],
45
+ }
46
+
47
+ @property
48
+ def required_permission(self) -> PermissionLevel:
49
+ return PermissionLevel.FULL_ACCESS
50
+
51
+ @property
52
+ def input_model(self) -> type[CronCreateInput]:
53
+ return CronCreateInput
54
+
55
+ def execute(self, args: dict) -> ToolResult:
56
+ cron_expr = args["cron"]
57
+ prompt = args["prompt"]
58
+ recurring = args.get("recurring", True)
59
+ permanent = args.get("permanent", False)
60
+
61
+ # Validate cron expression
62
+ try:
63
+ parse_cron(cron_expr)
64
+ except ValueError as exc:
65
+ return ToolResult(output=f"Invalid cron expression: {exc}", is_error=True)
66
+
67
+ try:
68
+ task = self._storage.add(
69
+ cron=cron_expr,
70
+ prompt=prompt,
71
+ recurring=recurring,
72
+ permanent=permanent,
73
+ )
74
+ except ValueError as exc:
75
+ return ToolResult(output=str(exc), is_error=True)
76
+
77
+ flags = []
78
+ if recurring:
79
+ flags.append("recurring")
80
+ if permanent:
81
+ flags.append("permanent")
82
+ flag_str = f" ({', '.join(flags)})" if flags else ""
83
+
84
+ return ToolResult(
85
+ output=(
86
+ f"Scheduled task {task.id}{flag_str}\n"
87
+ f" Cron: {task.cron}\n"
88
+ f" Prompt: {task.prompt}"
89
+ )
90
+ )
@@ -0,0 +1,49 @@
1
+ """CronDeleteTool — delete a scheduled cron task."""
2
+ from __future__ import annotations
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from llm_code.cron.storage import CronStorage
7
+ from llm_code.tools.base import PermissionLevel, Tool, ToolResult
8
+
9
+
10
+ class CronDeleteInput(BaseModel):
11
+ task_id: str
12
+
13
+
14
+ class CronDeleteTool(Tool):
15
+ def __init__(self, storage: CronStorage) -> None:
16
+ self._storage = storage
17
+
18
+ @property
19
+ def name(self) -> str:
20
+ return "cron_delete"
21
+
22
+ @property
23
+ def description(self) -> str:
24
+ return "Delete a scheduled cron task by ID."
25
+
26
+ @property
27
+ def input_schema(self) -> dict:
28
+ return {
29
+ "type": "object",
30
+ "properties": {
31
+ "task_id": {"type": "string", "description": "ID of the task to delete"},
32
+ },
33
+ "required": ["task_id"],
34
+ }
35
+
36
+ @property
37
+ def required_permission(self) -> PermissionLevel:
38
+ return PermissionLevel.FULL_ACCESS
39
+
40
+ @property
41
+ def input_model(self) -> type[CronDeleteInput]:
42
+ return CronDeleteInput
43
+
44
+ def execute(self, args: dict) -> ToolResult:
45
+ task_id = args["task_id"]
46
+ removed = self._storage.remove(task_id)
47
+ if not removed:
48
+ return ToolResult(output=f"Task '{task_id}' not found", is_error=True)
49
+ return ToolResult(output=f"Deleted task {task_id}")
@@ -0,0 +1,51 @@
1
+ """CronListTool — list all scheduled cron tasks."""
2
+ from __future__ import annotations
3
+
4
+ from llm_code.cron.storage import CronStorage
5
+ from llm_code.tools.base import PermissionLevel, Tool, ToolResult
6
+
7
+
8
+ class CronListTool(Tool):
9
+ def __init__(self, storage: CronStorage) -> None:
10
+ self._storage = storage
11
+
12
+ @property
13
+ def name(self) -> str:
14
+ return "cron_list"
15
+
16
+ @property
17
+ def description(self) -> str:
18
+ return "List all scheduled cron tasks with their status."
19
+
20
+ @property
21
+ def input_schema(self) -> dict:
22
+ return {"type": "object", "properties": {}}
23
+
24
+ @property
25
+ def required_permission(self) -> PermissionLevel:
26
+ return PermissionLevel.READ_ONLY
27
+
28
+ def is_read_only(self, args: dict) -> bool:
29
+ return True
30
+
31
+ def is_concurrency_safe(self, args: dict) -> bool:
32
+ return True
33
+
34
+ def execute(self, args: dict) -> ToolResult:
35
+ tasks = self._storage.list_all()
36
+ if not tasks:
37
+ return ToolResult(output="No scheduled tasks.")
38
+
39
+ lines: list[str] = [f"Scheduled tasks ({len(tasks)}):"]
40
+ for t in tasks:
41
+ flags = []
42
+ if t.recurring:
43
+ flags.append("recurring")
44
+ if t.permanent:
45
+ flags.append("permanent")
46
+ flag_str = f" [{', '.join(flags)}]" if flags else ""
47
+ fired = f", last fired: {t.last_fired_at:%Y-%m-%d %H:%M}" if t.last_fired_at else ""
48
+ lines.append(
49
+ f" {t.id} {t.cron} \"{t.prompt}\"{flag_str}{fired}"
50
+ )
51
+ return ToolResult(output="\n".join(lines))
@@ -0,0 +1,92 @@
1
+ """Deferred tool loading: keeps visible tool list small by hiding rarely-used tools."""
2
+ from __future__ import annotations
3
+
4
+ from llm_code.api.types import ToolDefinition
5
+
6
+ # Core tools always visible regardless of max_visible limit
7
+ CORE_TOOLS: frozenset[str] = frozenset(
8
+ {
9
+ "read_file",
10
+ "write_file",
11
+ "edit_file",
12
+ "glob_search",
13
+ "grep_search",
14
+ "bash",
15
+ "agent",
16
+ "tool_search",
17
+ }
18
+ )
19
+
20
+
21
+ class DeferredToolManager:
22
+ """Manages splitting tool definitions into visible and deferred sets.
23
+
24
+ Core tools are always visible. Additional tools are shown up to max_visible;
25
+ the rest are deferred until the LLM calls tool_search to unlock them.
26
+ """
27
+
28
+ def __init__(self) -> None:
29
+ self._unlocked: set[str] = set()
30
+ # Track the last deferred list for search
31
+ self._deferred: list[ToolDefinition] = []
32
+
33
+ def select_tools(
34
+ self,
35
+ all_defs: list[ToolDefinition],
36
+ max_visible: int = 20,
37
+ ) -> tuple[list[ToolDefinition], list[ToolDefinition]]:
38
+ """Partition tool definitions into (visible, deferred).
39
+
40
+ Core tools and any unlocked tools are always visible. Non-core tools
41
+ fill remaining slots up to max_visible; the rest go to deferred.
42
+
43
+ Returns:
44
+ A 2-tuple of (visible_defs, deferred_defs).
45
+ """
46
+ all_names = {d.name for d in all_defs}
47
+
48
+ # Always-visible: core tools present in all_defs + previously unlocked
49
+ always_visible_names = (CORE_TOOLS | self._unlocked) & all_names
50
+
51
+ visible: list[ToolDefinition] = []
52
+ deferred: list[ToolDefinition] = []
53
+
54
+ for d in all_defs:
55
+ if d.name in always_visible_names:
56
+ visible.append(d)
57
+
58
+ # Fill remaining slots with non-core, non-unlocked tools
59
+ remaining_slots = max_visible - len(visible)
60
+ for d in all_defs:
61
+ if d.name not in always_visible_names:
62
+ if remaining_slots > 0:
63
+ visible.append(d)
64
+ remaining_slots -= 1
65
+ else:
66
+ deferred.append(d)
67
+
68
+ # Store for search
69
+ self._deferred = deferred
70
+ return visible, deferred
71
+
72
+ def search_tools(
73
+ self, query: str, deferred: list[ToolDefinition]
74
+ ) -> list[ToolDefinition]:
75
+ """Fuzzy-match query against name and description of deferred tools.
76
+
77
+ Matching is case-insensitive substring search against both the tool
78
+ name and description.
79
+
80
+ Returns:
81
+ List of matching ToolDefinition objects.
82
+ """
83
+ q = query.lower()
84
+ results: list[ToolDefinition] = []
85
+ for d in deferred:
86
+ if q in d.name.lower() or q in d.description.lower():
87
+ results.append(d)
88
+ return results
89
+
90
+ def unlock_tool(self, name: str) -> None:
91
+ """Mark a tool as unlocked so it appears in visible set on future calls."""
92
+ self._unlocked.add(name)