squidbot 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.
- squidbot/__init__.py +5 -0
- squidbot/agent.py +263 -0
- squidbot/channels.py +271 -0
- squidbot/character.py +83 -0
- squidbot/client.py +318 -0
- squidbot/config.py +148 -0
- squidbot/daemon.py +310 -0
- squidbot/lanes.py +41 -0
- squidbot/main.py +157 -0
- squidbot/memory_db.py +706 -0
- squidbot/playwright_check.py +233 -0
- squidbot/plugins/__init__.py +47 -0
- squidbot/plugins/base.py +96 -0
- squidbot/plugins/hooks.py +416 -0
- squidbot/plugins/loader.py +248 -0
- squidbot/plugins/web3_plugin.py +407 -0
- squidbot/scheduler.py +214 -0
- squidbot/server.py +487 -0
- squidbot/session.py +609 -0
- squidbot/skills.py +141 -0
- squidbot/skills_template/reminder/SKILL.md +13 -0
- squidbot/skills_template/search/SKILL.md +11 -0
- squidbot/skills_template/summarize/SKILL.md +14 -0
- squidbot/tools/__init__.py +100 -0
- squidbot/tools/base.py +42 -0
- squidbot/tools/browser.py +311 -0
- squidbot/tools/coding.py +599 -0
- squidbot/tools/cron.py +218 -0
- squidbot/tools/memory_tool.py +152 -0
- squidbot/tools/web_search.py +50 -0
- squidbot-0.1.0.dist-info/METADATA +542 -0
- squidbot-0.1.0.dist-info/RECORD +34 -0
- squidbot-0.1.0.dist-info/WHEEL +4 -0
- squidbot-0.1.0.dist-info/entry_points.txt +4 -0
squidbot/tools/coding.py
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
"""Coding agent tools for Zig and Python with workspace support."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from ..config import DATA_DIR
|
|
13
|
+
from .base import Tool
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Workspace directory
|
|
18
|
+
WORKSPACE_DIR = DATA_DIR / "coding"
|
|
19
|
+
|
|
20
|
+
# Supported languages
|
|
21
|
+
Language = Literal["zig", "python"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_workspace() -> Path:
|
|
25
|
+
"""Get or create the coding workspace directory."""
|
|
26
|
+
WORKSPACE_DIR.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
return WORKSPACE_DIR
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_project_dir(project_name: str) -> Path:
|
|
31
|
+
"""Get project directory within workspace."""
|
|
32
|
+
project_dir = get_workspace() / project_name
|
|
33
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
return project_dir
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def run_command(
|
|
38
|
+
cmd: list[str],
|
|
39
|
+
cwd: Path | None = None,
|
|
40
|
+
timeout: int = 30,
|
|
41
|
+
) -> tuple[int, str, str]:
|
|
42
|
+
"""Run a command asynchronously with timeout."""
|
|
43
|
+
try:
|
|
44
|
+
process = await asyncio.create_subprocess_exec(
|
|
45
|
+
*cmd,
|
|
46
|
+
cwd=cwd,
|
|
47
|
+
stdout=asyncio.subprocess.PIPE,
|
|
48
|
+
stderr=asyncio.subprocess.PIPE,
|
|
49
|
+
)
|
|
50
|
+
stdout, stderr = await asyncio.wait_for(
|
|
51
|
+
process.communicate(),
|
|
52
|
+
timeout=timeout,
|
|
53
|
+
)
|
|
54
|
+
return (
|
|
55
|
+
process.returncode or 0,
|
|
56
|
+
stdout.decode("utf-8", errors="replace"),
|
|
57
|
+
stderr.decode("utf-8", errors="replace"),
|
|
58
|
+
)
|
|
59
|
+
except asyncio.TimeoutError:
|
|
60
|
+
process.kill()
|
|
61
|
+
return -1, "", f"Command timed out after {timeout}s"
|
|
62
|
+
except Exception as e:
|
|
63
|
+
return -1, "", str(e)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CodeWriteTool(Tool):
|
|
67
|
+
"""Write code to a file in the workspace."""
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def name(self) -> str:
|
|
71
|
+
return "code_write"
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def description(self) -> str:
|
|
75
|
+
return (
|
|
76
|
+
"Write code to a file in the coding workspace (.squidbot/coding/). "
|
|
77
|
+
"Supports Zig (.zig) and Python (.py) files. "
|
|
78
|
+
"Creates project directories automatically."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def parameters(self) -> dict:
|
|
83
|
+
return {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"properties": {
|
|
86
|
+
"project": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"description": "Project name (creates subdirectory)",
|
|
89
|
+
},
|
|
90
|
+
"filename": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "Filename with extension (.zig or .py)",
|
|
93
|
+
},
|
|
94
|
+
"code": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"description": "The code to write",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
"required": ["project", "filename", "code"],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async def execute(self, project: str, filename: str, code: str) -> str:
|
|
103
|
+
project_dir = get_project_dir(project)
|
|
104
|
+
file_path = project_dir / filename
|
|
105
|
+
|
|
106
|
+
# Validate extension
|
|
107
|
+
ext = file_path.suffix.lower()
|
|
108
|
+
if ext not in (".zig", ".py"):
|
|
109
|
+
return f"Error: Unsupported file type '{ext}'. Use .zig or .py"
|
|
110
|
+
|
|
111
|
+
# Write file
|
|
112
|
+
file_path.write_text(code, encoding="utf-8")
|
|
113
|
+
logger.info(f"Wrote {len(code)} bytes to {file_path}")
|
|
114
|
+
|
|
115
|
+
# Try to get relative path, fall back to absolute
|
|
116
|
+
try:
|
|
117
|
+
rel_path = file_path.relative_to(DATA_DIR.parent)
|
|
118
|
+
return f"Written to {rel_path}"
|
|
119
|
+
except ValueError:
|
|
120
|
+
return f"Written to {file_path}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CodeReadTool(Tool):
|
|
124
|
+
"""Read code from a file in the workspace."""
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def name(self) -> str:
|
|
128
|
+
return "code_read"
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def description(self) -> str:
|
|
132
|
+
return "Read code from a file in the coding workspace."
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def parameters(self) -> dict:
|
|
136
|
+
return {
|
|
137
|
+
"type": "object",
|
|
138
|
+
"properties": {
|
|
139
|
+
"project": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"description": "Project name",
|
|
142
|
+
},
|
|
143
|
+
"filename": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"description": "Filename to read",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
"required": ["project", "filename"],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async def execute(self, project: str, filename: str) -> str:
|
|
152
|
+
project_dir = get_project_dir(project)
|
|
153
|
+
file_path = project_dir / filename
|
|
154
|
+
|
|
155
|
+
if not file_path.exists():
|
|
156
|
+
return f"Error: File not found: {filename}"
|
|
157
|
+
|
|
158
|
+
code = file_path.read_text(encoding="utf-8")
|
|
159
|
+
return f"```{file_path.suffix[1:]}\n{code}\n```"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class CodeRunTool(Tool):
|
|
163
|
+
"""Run code in the workspace."""
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def name(self) -> str:
|
|
167
|
+
return "code_run"
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def description(self) -> str:
|
|
171
|
+
return (
|
|
172
|
+
"Run code in the workspace. "
|
|
173
|
+
"For Zig: compiles and runs .zig files. "
|
|
174
|
+
"For Python: executes .py files. "
|
|
175
|
+
"Returns stdout, stderr, and exit code."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def parameters(self) -> dict:
|
|
180
|
+
return {
|
|
181
|
+
"type": "object",
|
|
182
|
+
"properties": {
|
|
183
|
+
"project": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"description": "Project name",
|
|
186
|
+
},
|
|
187
|
+
"filename": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"description": "Filename to run",
|
|
190
|
+
},
|
|
191
|
+
"args": {
|
|
192
|
+
"type": "array",
|
|
193
|
+
"items": {"type": "string"},
|
|
194
|
+
"description": "Command line arguments (optional)",
|
|
195
|
+
},
|
|
196
|
+
"timeout": {
|
|
197
|
+
"type": "integer",
|
|
198
|
+
"description": "Timeout in seconds (default 30)",
|
|
199
|
+
"default": 30,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
"required": ["project", "filename"],
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async def execute(
|
|
206
|
+
self,
|
|
207
|
+
project: str,
|
|
208
|
+
filename: str,
|
|
209
|
+
args: list[str] | None = None,
|
|
210
|
+
timeout: int = 30,
|
|
211
|
+
) -> str:
|
|
212
|
+
project_dir = get_project_dir(project)
|
|
213
|
+
file_path = project_dir / filename
|
|
214
|
+
args = args or []
|
|
215
|
+
|
|
216
|
+
if not file_path.exists():
|
|
217
|
+
return f"Error: File not found: {filename}"
|
|
218
|
+
|
|
219
|
+
ext = file_path.suffix.lower()
|
|
220
|
+
|
|
221
|
+
if ext == ".zig":
|
|
222
|
+
return await self._run_zig(file_path, project_dir, args, timeout)
|
|
223
|
+
elif ext == ".py":
|
|
224
|
+
return await self._run_python(file_path, args, timeout)
|
|
225
|
+
else:
|
|
226
|
+
return f"Error: Cannot run '{ext}' files. Use .zig or .py"
|
|
227
|
+
|
|
228
|
+
async def _run_zig(
|
|
229
|
+
self,
|
|
230
|
+
file_path: Path,
|
|
231
|
+
project_dir: Path,
|
|
232
|
+
args: list[str],
|
|
233
|
+
timeout: int,
|
|
234
|
+
) -> str:
|
|
235
|
+
# Check if zig is available
|
|
236
|
+
zig_path = shutil.which("zig")
|
|
237
|
+
if not zig_path:
|
|
238
|
+
return "Error: Zig compiler not found. Please install Zig."
|
|
239
|
+
|
|
240
|
+
# Compile and run
|
|
241
|
+
cmd = ["zig", "run", str(file_path)] + args
|
|
242
|
+
code, stdout, stderr = await run_command(cmd, cwd=project_dir, timeout=timeout)
|
|
243
|
+
|
|
244
|
+
result = []
|
|
245
|
+
if stdout:
|
|
246
|
+
result.append(f"stdout:\n{stdout}")
|
|
247
|
+
if stderr:
|
|
248
|
+
result.append(f"stderr:\n{stderr}")
|
|
249
|
+
result.append(f"exit code: {code}")
|
|
250
|
+
|
|
251
|
+
return "\n".join(result) if result else "No output"
|
|
252
|
+
|
|
253
|
+
async def _run_python(
|
|
254
|
+
self,
|
|
255
|
+
file_path: Path,
|
|
256
|
+
args: list[str],
|
|
257
|
+
timeout: int,
|
|
258
|
+
) -> str:
|
|
259
|
+
# Use same Python interpreter
|
|
260
|
+
import sys
|
|
261
|
+
|
|
262
|
+
cmd = [sys.executable, str(file_path)] + args
|
|
263
|
+
code, stdout, stderr = await run_command(
|
|
264
|
+
cmd, cwd=file_path.parent, timeout=timeout
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
result = []
|
|
268
|
+
if stdout:
|
|
269
|
+
result.append(f"stdout:\n{stdout}")
|
|
270
|
+
if stderr:
|
|
271
|
+
result.append(f"stderr:\n{stderr}")
|
|
272
|
+
result.append(f"exit code: {code}")
|
|
273
|
+
|
|
274
|
+
return "\n".join(result) if result else "No output"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class CodeListTool(Tool):
|
|
278
|
+
"""List files in a project."""
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def name(self) -> str:
|
|
282
|
+
return "code_list"
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def description(self) -> str:
|
|
286
|
+
return "List files in a project or all projects in the workspace."
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def parameters(self) -> dict:
|
|
290
|
+
return {
|
|
291
|
+
"type": "object",
|
|
292
|
+
"properties": {
|
|
293
|
+
"project": {
|
|
294
|
+
"type": "string",
|
|
295
|
+
"description": "Project name (optional, lists all projects if omitted)",
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
"required": [],
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async def execute(self, project: str | None = None) -> str:
|
|
302
|
+
workspace = get_workspace()
|
|
303
|
+
|
|
304
|
+
if project:
|
|
305
|
+
project_dir = workspace / project
|
|
306
|
+
if not project_dir.exists():
|
|
307
|
+
return f"Project '{project}' not found"
|
|
308
|
+
|
|
309
|
+
files = sorted(project_dir.rglob("*"))
|
|
310
|
+
if not files:
|
|
311
|
+
return f"Project '{project}' is empty"
|
|
312
|
+
|
|
313
|
+
lines = [f"Files in {project}:"]
|
|
314
|
+
for f in files:
|
|
315
|
+
if f.is_file():
|
|
316
|
+
rel = f.relative_to(project_dir)
|
|
317
|
+
size = f.stat().st_size
|
|
318
|
+
lines.append(f" {rel} ({size} bytes)")
|
|
319
|
+
return "\n".join(lines)
|
|
320
|
+
else:
|
|
321
|
+
# List all projects
|
|
322
|
+
projects = sorted([d for d in workspace.iterdir() if d.is_dir()])
|
|
323
|
+
if not projects:
|
|
324
|
+
return "No projects in workspace"
|
|
325
|
+
|
|
326
|
+
lines = ["Projects:"]
|
|
327
|
+
for p in projects:
|
|
328
|
+
file_count = len(list(p.rglob("*")))
|
|
329
|
+
lines.append(f" {p.name}/ ({file_count} files)")
|
|
330
|
+
return "\n".join(lines)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class CodeDeleteTool(Tool):
|
|
334
|
+
"""Delete a file or project."""
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def name(self) -> str:
|
|
338
|
+
return "code_delete"
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def description(self) -> str:
|
|
342
|
+
return "Delete a file or entire project from the workspace."
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def parameters(self) -> dict:
|
|
346
|
+
return {
|
|
347
|
+
"type": "object",
|
|
348
|
+
"properties": {
|
|
349
|
+
"project": {
|
|
350
|
+
"type": "string",
|
|
351
|
+
"description": "Project name",
|
|
352
|
+
},
|
|
353
|
+
"filename": {
|
|
354
|
+
"type": "string",
|
|
355
|
+
"description": "Filename to delete (omit to delete entire project)",
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
"required": ["project"],
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async def execute(self, project: str, filename: str | None = None) -> str:
|
|
362
|
+
workspace = get_workspace()
|
|
363
|
+
project_dir = workspace / project
|
|
364
|
+
|
|
365
|
+
if not project_dir.exists():
|
|
366
|
+
return f"Project '{project}' not found"
|
|
367
|
+
|
|
368
|
+
if filename:
|
|
369
|
+
file_path = project_dir / filename
|
|
370
|
+
if not file_path.exists():
|
|
371
|
+
return f"File '{filename}' not found in {project}"
|
|
372
|
+
file_path.unlink()
|
|
373
|
+
return f"Deleted {filename} from {project}"
|
|
374
|
+
else:
|
|
375
|
+
shutil.rmtree(project_dir)
|
|
376
|
+
return f"Deleted project '{project}'"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class ZigBuildTool(Tool):
|
|
380
|
+
"""Build a Zig project."""
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def name(self) -> str:
|
|
384
|
+
return "zig_build"
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def description(self) -> str:
|
|
388
|
+
return (
|
|
389
|
+
"Build a Zig project. Can create optimized release builds. "
|
|
390
|
+
"For single files, compiles to executable. "
|
|
391
|
+
"For projects with build.zig, runs 'zig build'."
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def parameters(self) -> dict:
|
|
396
|
+
return {
|
|
397
|
+
"type": "object",
|
|
398
|
+
"properties": {
|
|
399
|
+
"project": {
|
|
400
|
+
"type": "string",
|
|
401
|
+
"description": "Project name",
|
|
402
|
+
},
|
|
403
|
+
"filename": {
|
|
404
|
+
"type": "string",
|
|
405
|
+
"description": "Main .zig file (optional if build.zig exists)",
|
|
406
|
+
},
|
|
407
|
+
"release": {
|
|
408
|
+
"type": "boolean",
|
|
409
|
+
"description": "Build optimized release (default false)",
|
|
410
|
+
"default": False,
|
|
411
|
+
},
|
|
412
|
+
"output": {
|
|
413
|
+
"type": "string",
|
|
414
|
+
"description": "Output executable name (optional)",
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
"required": ["project"],
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async def execute(
|
|
421
|
+
self,
|
|
422
|
+
project: str,
|
|
423
|
+
filename: str | None = None,
|
|
424
|
+
release: bool = False,
|
|
425
|
+
output: str | None = None,
|
|
426
|
+
) -> str:
|
|
427
|
+
project_dir = get_project_dir(project)
|
|
428
|
+
|
|
429
|
+
# Check if zig is available
|
|
430
|
+
zig_path = shutil.which("zig")
|
|
431
|
+
if not zig_path:
|
|
432
|
+
return "Error: Zig compiler not found. Please install Zig."
|
|
433
|
+
|
|
434
|
+
build_zig = project_dir / "build.zig"
|
|
435
|
+
|
|
436
|
+
if build_zig.exists():
|
|
437
|
+
# Use build.zig
|
|
438
|
+
cmd = ["zig", "build"]
|
|
439
|
+
if release:
|
|
440
|
+
cmd.append("-Doptimize=ReleaseFast")
|
|
441
|
+
elif filename:
|
|
442
|
+
# Single file build
|
|
443
|
+
file_path = project_dir / filename
|
|
444
|
+
if not file_path.exists():
|
|
445
|
+
return f"Error: File not found: {filename}"
|
|
446
|
+
|
|
447
|
+
out_name = output or file_path.stem
|
|
448
|
+
cmd = ["zig", "build-exe", str(file_path), f"-femit-bin={out_name}"]
|
|
449
|
+
if release:
|
|
450
|
+
cmd.append("-O")
|
|
451
|
+
cmd.append("ReleaseFast")
|
|
452
|
+
else:
|
|
453
|
+
return "Error: No build.zig found and no filename specified"
|
|
454
|
+
|
|
455
|
+
code, stdout, stderr = await run_command(cmd, cwd=project_dir, timeout=120)
|
|
456
|
+
|
|
457
|
+
result = []
|
|
458
|
+
if stdout:
|
|
459
|
+
result.append(f"stdout:\n{stdout}")
|
|
460
|
+
if stderr:
|
|
461
|
+
result.append(f"stderr:\n{stderr}")
|
|
462
|
+
|
|
463
|
+
if code == 0:
|
|
464
|
+
result.append("Build successful!")
|
|
465
|
+
else:
|
|
466
|
+
result.append(f"Build failed (exit code: {code})")
|
|
467
|
+
|
|
468
|
+
return "\n".join(result)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
class ZigTestTool(Tool):
|
|
472
|
+
"""Run Zig tests."""
|
|
473
|
+
|
|
474
|
+
@property
|
|
475
|
+
def name(self) -> str:
|
|
476
|
+
return "zig_test"
|
|
477
|
+
|
|
478
|
+
@property
|
|
479
|
+
def description(self) -> str:
|
|
480
|
+
return "Run Zig tests in a file or project."
|
|
481
|
+
|
|
482
|
+
@property
|
|
483
|
+
def parameters(self) -> dict:
|
|
484
|
+
return {
|
|
485
|
+
"type": "object",
|
|
486
|
+
"properties": {
|
|
487
|
+
"project": {
|
|
488
|
+
"type": "string",
|
|
489
|
+
"description": "Project name",
|
|
490
|
+
},
|
|
491
|
+
"filename": {
|
|
492
|
+
"type": "string",
|
|
493
|
+
"description": ".zig file with tests (optional if build.zig exists)",
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
"required": ["project"],
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async def execute(self, project: str, filename: str | None = None) -> str:
|
|
500
|
+
project_dir = get_project_dir(project)
|
|
501
|
+
|
|
502
|
+
zig_path = shutil.which("zig")
|
|
503
|
+
if not zig_path:
|
|
504
|
+
return "Error: Zig compiler not found. Please install Zig."
|
|
505
|
+
|
|
506
|
+
build_zig = project_dir / "build.zig"
|
|
507
|
+
|
|
508
|
+
if build_zig.exists() and not filename:
|
|
509
|
+
cmd = ["zig", "build", "test"]
|
|
510
|
+
elif filename:
|
|
511
|
+
file_path = project_dir / filename
|
|
512
|
+
if not file_path.exists():
|
|
513
|
+
return f"Error: File not found: {filename}"
|
|
514
|
+
cmd = ["zig", "test", str(file_path)]
|
|
515
|
+
else:
|
|
516
|
+
return "Error: No build.zig found and no filename specified"
|
|
517
|
+
|
|
518
|
+
code, stdout, stderr = await run_command(cmd, cwd=project_dir, timeout=60)
|
|
519
|
+
|
|
520
|
+
result = []
|
|
521
|
+
if stdout:
|
|
522
|
+
result.append(stdout)
|
|
523
|
+
if stderr:
|
|
524
|
+
result.append(stderr)
|
|
525
|
+
|
|
526
|
+
if code == 0:
|
|
527
|
+
result.append("All tests passed!")
|
|
528
|
+
else:
|
|
529
|
+
result.append(f"Tests failed (exit code: {code})")
|
|
530
|
+
|
|
531
|
+
return "\n".join(result)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
class PythonTestTool(Tool):
|
|
535
|
+
"""Run Python tests with pytest."""
|
|
536
|
+
|
|
537
|
+
@property
|
|
538
|
+
def name(self) -> str:
|
|
539
|
+
return "python_test"
|
|
540
|
+
|
|
541
|
+
@property
|
|
542
|
+
def description(self) -> str:
|
|
543
|
+
return "Run Python tests with pytest in a project."
|
|
544
|
+
|
|
545
|
+
@property
|
|
546
|
+
def parameters(self) -> dict:
|
|
547
|
+
return {
|
|
548
|
+
"type": "object",
|
|
549
|
+
"properties": {
|
|
550
|
+
"project": {
|
|
551
|
+
"type": "string",
|
|
552
|
+
"description": "Project name",
|
|
553
|
+
},
|
|
554
|
+
"filename": {
|
|
555
|
+
"type": "string",
|
|
556
|
+
"description": "Specific test file (optional)",
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
"required": ["project"],
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async def execute(self, project: str, filename: str | None = None) -> str:
|
|
563
|
+
import sys
|
|
564
|
+
|
|
565
|
+
project_dir = get_project_dir(project)
|
|
566
|
+
|
|
567
|
+
cmd = [sys.executable, "-m", "pytest", "-v"]
|
|
568
|
+
if filename:
|
|
569
|
+
file_path = project_dir / filename
|
|
570
|
+
if not file_path.exists():
|
|
571
|
+
return f"Error: File not found: {filename}"
|
|
572
|
+
cmd.append(str(file_path))
|
|
573
|
+
else:
|
|
574
|
+
cmd.append(str(project_dir))
|
|
575
|
+
|
|
576
|
+
code, stdout, stderr = await run_command(cmd, cwd=project_dir, timeout=120)
|
|
577
|
+
|
|
578
|
+
result = []
|
|
579
|
+
if stdout:
|
|
580
|
+
result.append(stdout)
|
|
581
|
+
if stderr:
|
|
582
|
+
result.append(stderr)
|
|
583
|
+
|
|
584
|
+
return "\n".join(result) if result else "No test output"
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
# Export all coding tools
|
|
588
|
+
def get_coding_tools() -> list[Tool]:
|
|
589
|
+
"""Get all coding agent tools."""
|
|
590
|
+
return [
|
|
591
|
+
CodeWriteTool(),
|
|
592
|
+
CodeReadTool(),
|
|
593
|
+
CodeRunTool(),
|
|
594
|
+
CodeListTool(),
|
|
595
|
+
CodeDeleteTool(),
|
|
596
|
+
ZigBuildTool(),
|
|
597
|
+
ZigTestTool(),
|
|
598
|
+
PythonTestTool(),
|
|
599
|
+
]
|