supyagent 0.2.0__py3-none-any.whl → 0.2.1__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.

Potentially problematic release.


This version of supyagent might be problematic. Click here for more details.

supyagent/cli/main.py CHANGED
@@ -21,13 +21,14 @@ from supyagent.core.config import ConfigManager, load_config
21
21
  from supyagent.core.executor import ExecutionRunner
22
22
  from supyagent.core.registry import AgentRegistry
23
23
  from supyagent.core.session_manager import SessionManager
24
+ from supyagent.default_tools import install_default_tools, list_default_tools
24
25
  from supyagent.models.agent_config import AgentNotFoundError, load_agent_config
25
26
 
26
27
  console = Console()
27
28
 
28
29
 
29
30
  @click.group()
30
- @click.version_option(version="0.2.0", prog_name="supyagent")
31
+ @click.version_option(version="0.2.1", prog_name="supyagent")
31
32
  def cli():
32
33
  """
33
34
  Supyagent - LLM agents powered by supypowers.
@@ -37,12 +38,82 @@ def cli():
37
38
  Quick start:
38
39
 
39
40
  \b
41
+ supyagent init # Set up default tools
42
+ supyagent config set # Configure API keys
40
43
  supyagent new myagent # Create an agent
41
44
  supyagent chat myagent # Start chatting
42
45
  """
43
46
  pass
44
47
 
45
48
 
49
+ @cli.command()
50
+ @click.option(
51
+ "--tools-dir",
52
+ "-t",
53
+ default="supypowers",
54
+ help="Directory for tools (default: supypowers/)",
55
+ )
56
+ @click.option(
57
+ "--force",
58
+ "-f",
59
+ is_flag=True,
60
+ help="Overwrite existing files",
61
+ )
62
+ def init(tools_dir: str, force: bool):
63
+ """
64
+ Initialize supyagent in the current directory.
65
+
66
+ This sets up:
67
+ - Default tools in supypowers/ (shell commands, file operations)
68
+ - agents/ directory for agent configurations
69
+
70
+ \b
71
+ Examples:
72
+ supyagent init
73
+ supyagent init --tools-dir my_tools
74
+ """
75
+ console.print("[bold]Initializing supyagent...[/bold]")
76
+ console.print()
77
+
78
+ # Create agents directory
79
+ agents_dir = Path("agents")
80
+ if not agents_dir.exists():
81
+ agents_dir.mkdir(parents=True)
82
+ console.print(f" [green]✓[/green] Created {agents_dir}/")
83
+ else:
84
+ console.print(f" [dim]○[/dim] {agents_dir}/ already exists")
85
+
86
+ # Install default tools
87
+ tools_path = Path(tools_dir)
88
+
89
+ if force:
90
+ # Remove and reinstall
91
+ import shutil
92
+
93
+ if tools_path.exists():
94
+ shutil.rmtree(tools_path)
95
+
96
+ if tools_path.exists() and any(tools_path.glob("*.py")):
97
+ console.print(f" [dim]○[/dim] {tools_dir}/ already has tools")
98
+ else:
99
+ count = install_default_tools(tools_path)
100
+ console.print(
101
+ f" [green]✓[/green] Installed {count} default tools to {tools_dir}/"
102
+ )
103
+
104
+ # Show available tools
105
+ console.print()
106
+ console.print("[bold]Available tools:[/bold]")
107
+ for tool in list_default_tools():
108
+ console.print(f" • [cyan]{tool['name']}[/cyan]: {tool['description']}")
109
+
110
+ console.print()
111
+ console.print("[bold]Next steps:[/bold]")
112
+ console.print(" 1. Configure your API key: [cyan]supyagent config set[/cyan]")
113
+ console.print(" 2. Create an agent: [cyan]supyagent new myagent[/cyan]")
114
+ console.print(" 3. Start chatting: [cyan]supyagent chat myagent[/cyan]")
115
+
116
+
46
117
  @cli.command()
47
118
  @click.argument("name")
48
119
  @click.option(
@@ -0,0 +1,74 @@
1
+ """
2
+ Default supypowers tools bundled with supyagent.
3
+
4
+ These tools are copied to the user's project when running `supyagent init`.
5
+ """
6
+
7
+ import shutil
8
+ from pathlib import Path
9
+
10
+ # Path to the bundled default tools
11
+ TOOLS_DIR = Path(__file__).parent
12
+
13
+
14
+ def get_bundled_tools() -> list[Path]:
15
+ """Get list of bundled tool files."""
16
+ return [f for f in TOOLS_DIR.glob("*.py") if f.name != "__init__.py"]
17
+
18
+
19
+ def install_default_tools(target_dir: Path | str = "supypowers") -> int:
20
+ """
21
+ Install default tools to a target directory.
22
+
23
+ Args:
24
+ target_dir: Directory to install tools to (default: supypowers/)
25
+
26
+ Returns:
27
+ Number of files installed
28
+ """
29
+ target = Path(target_dir)
30
+ target.mkdir(parents=True, exist_ok=True)
31
+
32
+ installed = 0
33
+ for tool_file in get_bundled_tools():
34
+ dest = target / tool_file.name
35
+ if not dest.exists():
36
+ shutil.copy(tool_file, dest)
37
+ installed += 1
38
+
39
+ # Create __init__.py if not exists
40
+ init_file = target / "__init__.py"
41
+ if not init_file.exists():
42
+ init_file.write_text('"""Supypowers tools for this project."""\n')
43
+ installed += 1
44
+
45
+ return installed
46
+
47
+
48
+ def list_default_tools() -> list[dict]:
49
+ """
50
+ List available default tools.
51
+
52
+ Returns:
53
+ List of tool info dicts
54
+ """
55
+ tools = []
56
+ for tool_file in get_bundled_tools():
57
+ # Read first docstring
58
+ content = tool_file.read_text()
59
+ description = ""
60
+ if '"""' in content:
61
+ start = content.find('"""') + 3
62
+ end = content.find('"""', start)
63
+ if end > start:
64
+ description = content[start:end].strip().split("\n")[0]
65
+
66
+ tools.append(
67
+ {
68
+ "name": tool_file.stem,
69
+ "file": tool_file.name,
70
+ "description": description,
71
+ }
72
+ )
73
+
74
+ return tools
@@ -0,0 +1,439 @@
1
+ # /// script
2
+ # dependencies = ["pydantic"]
3
+ # ///
4
+ """
5
+ File system operation tools.
6
+
7
+ Allows agents to read, write, and manage files and directories.
8
+ """
9
+
10
+ import os
11
+ import shutil
12
+ from pathlib import Path
13
+ from typing import Optional, List
14
+
15
+ from pydantic import BaseModel, Field
16
+
17
+
18
+ # =============================================================================
19
+ # Read File
20
+ # =============================================================================
21
+
22
+
23
+ class ReadFileInput(BaseModel):
24
+ """Input for read_file function."""
25
+
26
+ path: str = Field(description="Path to the file to read")
27
+ encoding: str = Field(default="utf-8", description="File encoding")
28
+
29
+
30
+ class ReadFileOutput(BaseModel):
31
+ """Output for read_file function."""
32
+
33
+ ok: bool
34
+ content: Optional[str] = None
35
+ size: Optional[int] = None
36
+ path: Optional[str] = None
37
+ error: Optional[str] = None
38
+
39
+
40
+ def read_file(input: ReadFileInput) -> ReadFileOutput:
41
+ """
42
+ Read the contents of a file.
43
+
44
+ Examples:
45
+ >>> read_file({"path": "README.md"})
46
+ >>> read_file({"path": "data.txt", "encoding": "latin-1"})
47
+ """
48
+ try:
49
+ path = os.path.expanduser(input.path)
50
+ p = Path(path)
51
+
52
+ if not p.exists():
53
+ return ReadFileOutput(ok=False, error=f"File not found: {path}")
54
+
55
+ if not p.is_file():
56
+ return ReadFileOutput(ok=False, error=f"Not a file: {path}")
57
+
58
+ size = p.stat().st_size
59
+ if size > 10 * 1024 * 1024:
60
+ return ReadFileOutput(
61
+ ok=False,
62
+ error=f"File too large ({size} bytes). Maximum is 10MB.",
63
+ )
64
+
65
+ content = p.read_text(encoding=input.encoding)
66
+ return ReadFileOutput(
67
+ ok=True,
68
+ content=content,
69
+ size=size,
70
+ path=str(p.absolute()),
71
+ )
72
+
73
+ except UnicodeDecodeError:
74
+ return ReadFileOutput(
75
+ ok=False,
76
+ error=f"Cannot decode file as {input.encoding}",
77
+ )
78
+ except PermissionError:
79
+ return ReadFileOutput(ok=False, error=f"Permission denied: {input.path}")
80
+ except Exception as e:
81
+ return ReadFileOutput(ok=False, error=str(e))
82
+
83
+
84
+ # =============================================================================
85
+ # Write File
86
+ # =============================================================================
87
+
88
+
89
+ class WriteFileInput(BaseModel):
90
+ """Input for write_file function."""
91
+
92
+ path: str = Field(description="Path to the file to write")
93
+ content: str = Field(description="Content to write")
94
+ encoding: str = Field(default="utf-8", description="File encoding")
95
+ create_dirs: bool = Field(
96
+ default=True, description="Create parent directories if needed"
97
+ )
98
+
99
+
100
+ class WriteFileOutput(BaseModel):
101
+ """Output for write_file function."""
102
+
103
+ ok: bool
104
+ path: Optional[str] = None
105
+ size: Optional[int] = None
106
+ error: Optional[str] = None
107
+
108
+
109
+ def write_file(input: WriteFileInput) -> WriteFileOutput:
110
+ """
111
+ Write content to a file.
112
+
113
+ Examples:
114
+ >>> write_file({"path": "output.txt", "content": "Hello, world!"})
115
+ >>> write_file({"path": "data/results.json", "content": '{"status": "ok"}'})
116
+ """
117
+ try:
118
+ path = os.path.expanduser(input.path)
119
+ p = Path(path)
120
+
121
+ if input.create_dirs:
122
+ p.parent.mkdir(parents=True, exist_ok=True)
123
+
124
+ p.write_text(input.content, encoding=input.encoding)
125
+
126
+ return WriteFileOutput(
127
+ ok=True,
128
+ path=str(p.absolute()),
129
+ size=len(input.content.encode(input.encoding)),
130
+ )
131
+
132
+ except PermissionError:
133
+ return WriteFileOutput(ok=False, error=f"Permission denied: {input.path}")
134
+ except Exception as e:
135
+ return WriteFileOutput(ok=False, error=str(e))
136
+
137
+
138
+ # =============================================================================
139
+ # List Directory
140
+ # =============================================================================
141
+
142
+
143
+ class ListDirectoryInput(BaseModel):
144
+ """Input for list_directory function."""
145
+
146
+ path: str = Field(default=".", description="Directory path")
147
+ pattern: Optional[str] = Field(
148
+ default=None, description="Optional glob pattern (e.g., '*.py')"
149
+ )
150
+ recursive: bool = Field(default=False, description="List recursively")
151
+
152
+
153
+ class FileInfo(BaseModel):
154
+ """Information about a single file/directory."""
155
+
156
+ name: str
157
+ path: str
158
+ type: str # "file" or "directory"
159
+ size: Optional[int] = None
160
+
161
+
162
+ class ListDirectoryOutput(BaseModel):
163
+ """Output for list_directory function."""
164
+
165
+ ok: bool
166
+ items: List[FileInfo] = []
167
+ count: int = 0
168
+ error: Optional[str] = None
169
+
170
+
171
+ def list_directory(input: ListDirectoryInput) -> ListDirectoryOutput:
172
+ """
173
+ List files and directories.
174
+
175
+ Examples:
176
+ >>> list_directory({"path": "."})
177
+ >>> list_directory({"path": "src", "pattern": "*.py"})
178
+ >>> list_directory({"path": ".", "pattern": "*.md", "recursive": True})
179
+ """
180
+ try:
181
+ path = os.path.expanduser(input.path)
182
+ p = Path(path)
183
+
184
+ if not p.exists():
185
+ return ListDirectoryOutput(
186
+ ok=False, error=f"Directory not found: {path}"
187
+ )
188
+
189
+ if not p.is_dir():
190
+ return ListDirectoryOutput(ok=False, error=f"Not a directory: {path}")
191
+
192
+ items = []
193
+
194
+ if input.pattern:
195
+ matches = p.rglob(input.pattern) if input.recursive else p.glob(input.pattern)
196
+ else:
197
+ matches = p.iterdir()
198
+
199
+ for item in sorted(matches):
200
+ try:
201
+ stat = item.stat()
202
+ items.append(
203
+ FileInfo(
204
+ name=item.name,
205
+ path=str(item),
206
+ type="directory" if item.is_dir() else "file",
207
+ size=stat.st_size if item.is_file() else None,
208
+ )
209
+ )
210
+ except (PermissionError, OSError):
211
+ pass # Skip inaccessible files
212
+
213
+ return ListDirectoryOutput(ok=True, items=items, count=len(items))
214
+
215
+ except Exception as e:
216
+ return ListDirectoryOutput(ok=False, error=str(e))
217
+
218
+
219
+ # =============================================================================
220
+ # File Info
221
+ # =============================================================================
222
+
223
+
224
+ class FileInfoInput(BaseModel):
225
+ """Input for file_info function."""
226
+
227
+ path: str = Field(description="Path to get info for")
228
+
229
+
230
+ class FileInfoOutput(BaseModel):
231
+ """Output for file_info function."""
232
+
233
+ ok: bool
234
+ path: Optional[str] = None
235
+ name: Optional[str] = None
236
+ type: Optional[str] = None
237
+ size: Optional[int] = None
238
+ extension: Optional[str] = None
239
+ error: Optional[str] = None
240
+
241
+
242
+ def file_info(input: FileInfoInput) -> FileInfoOutput:
243
+ """
244
+ Get detailed information about a file or directory.
245
+
246
+ Examples:
247
+ >>> file_info({"path": "README.md"})
248
+ """
249
+ try:
250
+ path = os.path.expanduser(input.path)
251
+ p = Path(path)
252
+
253
+ if not p.exists():
254
+ return FileInfoOutput(ok=False, error=f"Path not found: {path}")
255
+
256
+ stat = p.stat()
257
+
258
+ return FileInfoOutput(
259
+ ok=True,
260
+ path=str(p.absolute()),
261
+ name=p.name,
262
+ type="directory" if p.is_dir() else "file",
263
+ size=stat.st_size,
264
+ extension=p.suffix if p.is_file() else None,
265
+ )
266
+
267
+ except Exception as e:
268
+ return FileInfoOutput(ok=False, error=str(e))
269
+
270
+
271
+ # =============================================================================
272
+ # Delete File
273
+ # =============================================================================
274
+
275
+
276
+ class DeleteFileInput(BaseModel):
277
+ """Input for delete_file function."""
278
+
279
+ path: str = Field(description="Path to the file to delete")
280
+
281
+
282
+ class DeleteFileOutput(BaseModel):
283
+ """Output for delete_file function."""
284
+
285
+ ok: bool
286
+ deleted: Optional[str] = None
287
+ error: Optional[str] = None
288
+
289
+
290
+ def delete_file(input: DeleteFileInput) -> DeleteFileOutput:
291
+ """
292
+ Delete a file.
293
+
294
+ Examples:
295
+ >>> delete_file({"path": "temp.txt"})
296
+ """
297
+ try:
298
+ path = os.path.expanduser(input.path)
299
+ p = Path(path)
300
+
301
+ if not p.exists():
302
+ return DeleteFileOutput(ok=False, error=f"File not found: {path}")
303
+
304
+ if p.is_dir():
305
+ return DeleteFileOutput(
306
+ ok=False, error=f"Use delete_directory for directories: {path}"
307
+ )
308
+
309
+ p.unlink()
310
+ return DeleteFileOutput(ok=True, deleted=str(p))
311
+
312
+ except Exception as e:
313
+ return DeleteFileOutput(ok=False, error=str(e))
314
+
315
+
316
+ # =============================================================================
317
+ # Create Directory
318
+ # =============================================================================
319
+
320
+
321
+ class CreateDirectoryInput(BaseModel):
322
+ """Input for create_directory function."""
323
+
324
+ path: str = Field(description="Directory path to create")
325
+ parents: bool = Field(
326
+ default=True, description="Create parent directories if needed"
327
+ )
328
+
329
+
330
+ class CreateDirectoryOutput(BaseModel):
331
+ """Output for create_directory function."""
332
+
333
+ ok: bool
334
+ path: Optional[str] = None
335
+ error: Optional[str] = None
336
+
337
+
338
+ def create_directory(input: CreateDirectoryInput) -> CreateDirectoryOutput:
339
+ """
340
+ Create a directory.
341
+
342
+ Examples:
343
+ >>> create_directory({"path": "new_folder"})
344
+ >>> create_directory({"path": "a/b/c/deep"})
345
+ """
346
+ try:
347
+ path = os.path.expanduser(input.path)
348
+ p = Path(path)
349
+
350
+ p.mkdir(parents=input.parents, exist_ok=True)
351
+
352
+ return CreateDirectoryOutput(ok=True, path=str(p.absolute()))
353
+
354
+ except Exception as e:
355
+ return CreateDirectoryOutput(ok=False, error=str(e))
356
+
357
+
358
+ # =============================================================================
359
+ # Copy File
360
+ # =============================================================================
361
+
362
+
363
+ class CopyFileInput(BaseModel):
364
+ """Input for copy_file function."""
365
+
366
+ source: str = Field(description="Source file path")
367
+ destination: str = Field(description="Destination path")
368
+
369
+
370
+ class CopyFileOutput(BaseModel):
371
+ """Output for copy_file function."""
372
+
373
+ ok: bool
374
+ source: Optional[str] = None
375
+ destination: Optional[str] = None
376
+ error: Optional[str] = None
377
+
378
+
379
+ def copy_file(input: CopyFileInput) -> CopyFileOutput:
380
+ """
381
+ Copy a file.
382
+
383
+ Examples:
384
+ >>> copy_file({"source": "file.txt", "destination": "backup/file.txt"})
385
+ """
386
+ try:
387
+ source = os.path.expanduser(input.source)
388
+ destination = os.path.expanduser(input.destination)
389
+
390
+ # Create destination directory if needed
391
+ dest_path = Path(destination)
392
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
393
+
394
+ shutil.copy2(source, destination)
395
+
396
+ return CopyFileOutput(ok=True, source=source, destination=destination)
397
+
398
+ except Exception as e:
399
+ return CopyFileOutput(ok=False, error=str(e))
400
+
401
+
402
+ # =============================================================================
403
+ # Move File
404
+ # =============================================================================
405
+
406
+
407
+ class MoveFileInput(BaseModel):
408
+ """Input for move_file function."""
409
+
410
+ source: str = Field(description="Source path")
411
+ destination: str = Field(description="Destination path")
412
+
413
+
414
+ class MoveFileOutput(BaseModel):
415
+ """Output for move_file function."""
416
+
417
+ ok: bool
418
+ source: Optional[str] = None
419
+ destination: Optional[str] = None
420
+ error: Optional[str] = None
421
+
422
+
423
+ def move_file(input: MoveFileInput) -> MoveFileOutput:
424
+ """
425
+ Move or rename a file or directory.
426
+
427
+ Examples:
428
+ >>> move_file({"source": "old.txt", "destination": "new.txt"})
429
+ """
430
+ try:
431
+ source = os.path.expanduser(input.source)
432
+ destination = os.path.expanduser(input.destination)
433
+
434
+ shutil.move(source, destination)
435
+
436
+ return MoveFileOutput(ok=True, source=source, destination=destination)
437
+
438
+ except Exception as e:
439
+ return MoveFileOutput(ok=False, error=str(e))
@@ -0,0 +1,217 @@
1
+ # /// script
2
+ # dependencies = ["pydantic"]
3
+ # ///
4
+ """
5
+ Shell command execution tools.
6
+
7
+ Allows agents to run bash/shell commands on the system.
8
+ Use with caution - consider restricting via agent tool permissions.
9
+ """
10
+
11
+ import os
12
+ import subprocess
13
+ import shutil
14
+ from typing import Optional
15
+
16
+ from pydantic import BaseModel, Field
17
+
18
+
19
+ class RunCommandInput(BaseModel):
20
+ """Input for run_command function."""
21
+
22
+ command: str = Field(description="The shell command to execute")
23
+ working_dir: Optional[str] = Field(
24
+ default=None, description="Working directory for the command"
25
+ )
26
+ timeout: int = Field(
27
+ default=60, description="Maximum seconds to wait for command"
28
+ )
29
+
30
+
31
+ class RunCommandOutput(BaseModel):
32
+ """Output for run_command function."""
33
+
34
+ ok: bool
35
+ stdout: str
36
+ stderr: str
37
+ exit_code: int
38
+ command: str
39
+
40
+
41
+ def run_command(input: RunCommandInput) -> RunCommandOutput:
42
+ """
43
+ Execute a shell command and return the result.
44
+
45
+ Examples:
46
+ >>> run_command({"command": "ls -la"})
47
+ >>> run_command({"command": "echo 'hello' | grep hello"})
48
+ >>> run_command({"command": "pwd", "working_dir": "/tmp"})
49
+ """
50
+ try:
51
+ working_dir = input.working_dir
52
+ if working_dir:
53
+ working_dir = os.path.expanduser(working_dir)
54
+ working_dir = os.path.expandvars(working_dir)
55
+
56
+ result = subprocess.run(
57
+ input.command,
58
+ shell=True,
59
+ capture_output=True,
60
+ text=True,
61
+ timeout=input.timeout,
62
+ cwd=working_dir,
63
+ )
64
+
65
+ return RunCommandOutput(
66
+ ok=result.returncode == 0,
67
+ stdout=result.stdout,
68
+ stderr=result.stderr,
69
+ exit_code=result.returncode,
70
+ command=input.command,
71
+ )
72
+
73
+ except subprocess.TimeoutExpired:
74
+ return RunCommandOutput(
75
+ ok=False,
76
+ stdout="",
77
+ stderr=f"Command timed out after {input.timeout} seconds",
78
+ exit_code=-1,
79
+ command=input.command,
80
+ )
81
+ except Exception as e:
82
+ return RunCommandOutput(
83
+ ok=False,
84
+ stdout="",
85
+ stderr=str(e),
86
+ exit_code=-1,
87
+ command=input.command,
88
+ )
89
+
90
+
91
+ class RunScriptInput(BaseModel):
92
+ """Input for run_script function."""
93
+
94
+ script: str = Field(description="Multi-line script content")
95
+ interpreter: str = Field(
96
+ default="/bin/bash", description="Script interpreter"
97
+ )
98
+ working_dir: Optional[str] = Field(
99
+ default=None, description="Working directory"
100
+ )
101
+ timeout: int = Field(default=120, description="Max seconds to wait")
102
+
103
+
104
+ class RunScriptOutput(BaseModel):
105
+ """Output for run_script function."""
106
+
107
+ ok: bool
108
+ stdout: str
109
+ stderr: str
110
+ exit_code: int
111
+
112
+
113
+ def run_script(input: RunScriptInput) -> RunScriptOutput:
114
+ """
115
+ Execute a multi-line shell script.
116
+
117
+ Examples:
118
+ >>> run_script({"script": "cd /tmp\\necho $(pwd)\\nls"})
119
+ """
120
+ try:
121
+ working_dir = input.working_dir
122
+ if working_dir:
123
+ working_dir = os.path.expanduser(working_dir)
124
+
125
+ result = subprocess.run(
126
+ [input.interpreter],
127
+ input=input.script,
128
+ capture_output=True,
129
+ text=True,
130
+ timeout=input.timeout,
131
+ cwd=working_dir,
132
+ )
133
+
134
+ return RunScriptOutput(
135
+ ok=result.returncode == 0,
136
+ stdout=result.stdout,
137
+ stderr=result.stderr,
138
+ exit_code=result.returncode,
139
+ )
140
+
141
+ except subprocess.TimeoutExpired:
142
+ return RunScriptOutput(
143
+ ok=False,
144
+ stdout="",
145
+ stderr=f"Script timed out after {input.timeout} seconds",
146
+ exit_code=-1,
147
+ )
148
+ except Exception as e:
149
+ return RunScriptOutput(
150
+ ok=False,
151
+ stdout="",
152
+ stderr=str(e),
153
+ exit_code=-1,
154
+ )
155
+
156
+
157
+ class WhichInput(BaseModel):
158
+ """Input for which function."""
159
+
160
+ command: str = Field(description="Command name to look up")
161
+
162
+
163
+ class WhichOutput(BaseModel):
164
+ """Output for which function."""
165
+
166
+ ok: bool
167
+ path: Optional[str] = None
168
+ error: Optional[str] = None
169
+
170
+
171
+ def which(input: WhichInput) -> WhichOutput:
172
+ """
173
+ Find the path to an executable.
174
+
175
+ Examples:
176
+ >>> which({"command": "python"})
177
+ >>> which({"command": "git"})
178
+ """
179
+ path = shutil.which(input.command)
180
+ if path:
181
+ return WhichOutput(ok=True, path=path)
182
+ else:
183
+ return WhichOutput(ok=False, error=f"Command not found: {input.command}")
184
+
185
+
186
+ class GetEnvInput(BaseModel):
187
+ """Input for get_env function."""
188
+
189
+ name: str = Field(description="Environment variable name")
190
+ default: Optional[str] = Field(
191
+ default=None, description="Default if not set"
192
+ )
193
+
194
+
195
+ class GetEnvOutput(BaseModel):
196
+ """Output for get_env function."""
197
+
198
+ ok: bool
199
+ value: Optional[str] = None
200
+ error: Optional[str] = None
201
+
202
+
203
+ def get_env(input: GetEnvInput) -> GetEnvOutput:
204
+ """
205
+ Get an environment variable value.
206
+
207
+ Examples:
208
+ >>> get_env({"name": "HOME"})
209
+ >>> get_env({"name": "MY_VAR", "default": "fallback"})
210
+ """
211
+ value = os.environ.get(input.name, input.default)
212
+ if value is not None:
213
+ return GetEnvOutput(ok=True, value=value)
214
+ else:
215
+ return GetEnvOutput(
216
+ ok=False, error=f"Environment variable not set: {input.name}"
217
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supyagent
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: LLM agents powered by supypowers - build AI agents with tool use, multi-agent orchestration, and secure credential management
5
5
  Project-URL: Homepage, https://github.com/ergodic-ai/supyagent
6
6
  Project-URL: Documentation, https://github.com/ergodic-ai/supyagent#readme
@@ -68,6 +68,9 @@ uv pip install supyagent
68
68
  ## Quick Start
69
69
 
70
70
  ```bash
71
+ # Initialize supyagent (sets up default tools)
72
+ supyagent init
73
+
71
74
  # Set up your API key (stored securely)
72
75
  supyagent config set ANTHROPIC_API_KEY
73
76
 
@@ -229,6 +232,7 @@ delegates:
229
232
 
230
233
  | Command | Description |
231
234
  |---------|-------------|
235
+ | `supyagent init` | Initialize project with default tools |
232
236
  | `supyagent new <name>` | Create a new agent |
233
237
  | `supyagent list` | List all agents |
234
238
  | `supyagent show <name>` | Show agent details |
@@ -281,6 +285,25 @@ While chatting, use these commands:
281
285
  | `/clear` | Clear conversation history |
282
286
  | `/quit` | Exit the chat |
283
287
 
288
+ ## Bundled Tools
289
+
290
+ Running `supyagent init` installs these default tools:
291
+
292
+ ### Shell (`shell.py`)
293
+ - `run_command` - Execute shell commands
294
+ - `run_script` - Run multi-line bash scripts
295
+ - `which` - Find executable paths
296
+ - `get_env` - Get environment variables
297
+
298
+ ### Files (`files.py`)
299
+ - `read_file` / `write_file` - File I/O
300
+ - `list_directory` - List files with glob patterns
301
+ - `copy_file` / `move_file` / `delete_file` - File operations
302
+ - `create_directory` - Create directories
303
+ - `file_info` - Get file metadata
304
+
305
+ You can add your own tools by creating Python files in `supypowers/`.
306
+
284
307
  ## Project Structure
285
308
 
286
309
  ```
@@ -290,8 +313,9 @@ your-project/
290
313
  │ ├── planner.yaml
291
314
  │ └── researcher.yaml
292
315
  ├── supypowers/ # Tool definitions (Python)
293
- │ ├── hello.py
294
- └── web_search.py
316
+ │ ├── shell.py # Shell commands (bundled)
317
+ ├── files.py # File operations (bundled)
318
+ │ └── my_tools.py # Your custom tools
295
319
  └── .supyagent/ # Runtime data (gitignore this)
296
320
  ├── sessions/ # Conversation history
297
321
  ├── credentials/ # Encrypted secrets
@@ -1,7 +1,7 @@
1
1
  supyagent/__init__.py,sha256=JrrQbk1bmDhUb2qOqxqDGBq1V-oArwVS4f5f9Qu-VcM,77
2
2
  supyagent/__main__.py,sha256=szUvsccEIA428c2_5J_X1qzq4L44qLvDvHfuRPYoy7c,121
3
3
  supyagent/cli/__init__.py,sha256=7jb5kxHtzce4rNKd9zrAiATp3Zxj7UQvtpQsyA97BTc,32
4
- supyagent/cli/main.py,sha256=xV34tTDORoFnp-MU_l0e_RP675bWeUdh3ohVB9uVsBg,36328
4
+ supyagent/cli/main.py,sha256=6jXcEZjLRgE_9PnPvyg-6BWGg5_yB71xZOD8_cejSno,38515
5
5
  supyagent/core/__init__.py,sha256=7MI6K2-OR_Fg0wcMziC4u3gNa0YxWmzJXO0RtOCevUg,727
6
6
  supyagent/core/agent.py,sha256=mkiV6CnK6Vk0Wg-DiAMm29wAViMa52-1Yb33Y5-FvQY,12655
7
7
  supyagent/core/config.py,sha256=kuTpuxy-qr-fhou7rJ-JLF82cdzdHQTu46kCZccSw1Y,10041
@@ -13,13 +13,16 @@ supyagent/core/llm.py,sha256=z16cjqjPfwRGtEpUhZe1XlzOXJdlgKHjckNdeJnEWYw,1898
13
13
  supyagent/core/registry.py,sha256=1CGCPsOcrIc_63_oJaFmHY8WwMgJEvbwsv8mnsOBWtY,7326
14
14
  supyagent/core/session_manager.py,sha256=ngqrbofR8V0wZSuOfGHTtSYE0C9U6noDrzfDsO8wi24,7389
15
15
  supyagent/core/tools.py,sha256=exIynhwulsMJTzzW5NmDqBeXepULAt4FlVhGbtrvdXE,6627
16
+ supyagent/default_tools/__init__.py,sha256=VwVjwNSPxjflmzJ-Mv_7L79Yjn9LdxyeXNUkWzu3wFo,1890
17
+ supyagent/default_tools/files.py,sha256=tUpjkP7TKbcK75lQ6jzEDpVDBSeupzTSqx_g2-rkON0,12059
18
+ supyagent/default_tools/shell.py,sha256=fYXdy693Nb0hsNLMzTFgj3zaHq0wFSQ3SxpqHbVayjM,5478
16
19
  supyagent/models/__init__.py,sha256=y7fIdzh-yD0LKAK0zfY_MZdtjJhxofwRWBqrHDU93h0,259
17
20
  supyagent/models/agent_config.py,sha256=sQ7S7zExoFs0g-IMgACsGH2SMiDRjJTnCVZwhN6DnkY,2570
18
21
  supyagent/models/session.py,sha256=_Dwuija_Uvt_KzcNp82ja8qP5ltNHHz43Llug1vq1ic,1136
19
22
  supyagent/utils/__init__.py,sha256=l4HdE3n8axphMInBex1J1dB_O1MjzbZz6ot1l8Syf0g,39
20
23
  supyagent/utils/paths.py,sha256=Xum9kX_UjRvmshifSq423QsAKynpnyAVfpWuFjlsdDQ,692
21
- supyagent-0.2.0.dist-info/METADATA,sha256=lnle2YIGJXssPNm8jllCvhA_202_j3DL9CUNFiRGHO0,9562
22
- supyagent-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
23
- supyagent-0.2.0.dist-info/entry_points.txt,sha256=7dFXWtRewhLfA5XUckV7Yu1E5_c6c9lq8Gz-7BFs6TM,53
24
- supyagent-0.2.0.dist-info/licenses/LICENSE,sha256=35fw1cvTM-IhiR3xaUohbiFBc2OBNRjI5z1b1cF7vZI,1067
25
- supyagent-0.2.0.dist-info/RECORD,,
24
+ supyagent-0.2.1.dist-info/METADATA,sha256=5sn1RwCPZ0Ja2ZGLatdfHlgg7KZnCr3v0u7zywAgpSs,10387
25
+ supyagent-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
26
+ supyagent-0.2.1.dist-info/entry_points.txt,sha256=7dFXWtRewhLfA5XUckV7Yu1E5_c6c9lq8Gz-7BFs6TM,53
27
+ supyagent-0.2.1.dist-info/licenses/LICENSE,sha256=35fw1cvTM-IhiR3xaUohbiFBc2OBNRjI5z1b1cF7vZI,1067
28
+ supyagent-0.2.1.dist-info/RECORD,,