supyagent 0.2.0__py3-none-any.whl → 0.2.2__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,15 @@ 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()
28
+ console_err = Console(stderr=True)
27
29
 
28
30
 
29
31
  @click.group()
30
- @click.version_option(version="0.2.0", prog_name="supyagent")
32
+ @click.version_option(version="0.2.2", prog_name="supyagent")
31
33
  def cli():
32
34
  """
33
35
  Supyagent - LLM agents powered by supypowers.
@@ -37,12 +39,82 @@ def cli():
37
39
  Quick start:
38
40
 
39
41
  \b
42
+ supyagent init # Set up default tools
43
+ supyagent config set # Configure API keys
40
44
  supyagent new myagent # Create an agent
41
45
  supyagent chat myagent # Start chatting
42
46
  """
43
47
  pass
44
48
 
45
49
 
50
+ @cli.command()
51
+ @click.option(
52
+ "--tools-dir",
53
+ "-t",
54
+ default="supypowers",
55
+ help="Directory for tools (default: supypowers/)",
56
+ )
57
+ @click.option(
58
+ "--force",
59
+ "-f",
60
+ is_flag=True,
61
+ help="Overwrite existing files",
62
+ )
63
+ def init(tools_dir: str, force: bool):
64
+ """
65
+ Initialize supyagent in the current directory.
66
+
67
+ This sets up:
68
+ - Default tools in supypowers/ (shell commands, file operations)
69
+ - agents/ directory for agent configurations
70
+
71
+ \b
72
+ Examples:
73
+ supyagent init
74
+ supyagent init --tools-dir my_tools
75
+ """
76
+ console.print("[bold]Initializing supyagent...[/bold]")
77
+ console.print()
78
+
79
+ # Create agents directory
80
+ agents_dir = Path("agents")
81
+ if not agents_dir.exists():
82
+ agents_dir.mkdir(parents=True)
83
+ console.print(f" [green]✓[/green] Created {agents_dir}/")
84
+ else:
85
+ console.print(f" [dim]○[/dim] {agents_dir}/ already exists")
86
+
87
+ # Install default tools
88
+ tools_path = Path(tools_dir)
89
+
90
+ if force:
91
+ # Remove and reinstall
92
+ import shutil
93
+
94
+ if tools_path.exists():
95
+ shutil.rmtree(tools_path)
96
+
97
+ if tools_path.exists() and any(tools_path.glob("*.py")):
98
+ console.print(f" [dim]○[/dim] {tools_dir}/ already has tools")
99
+ else:
100
+ count = install_default_tools(tools_path)
101
+ console.print(
102
+ f" [green]✓[/green] Installed {count} default tools to {tools_dir}/"
103
+ )
104
+
105
+ # Show available tools
106
+ console.print()
107
+ console.print("[bold]Available tools:[/bold]")
108
+ for tool in list_default_tools():
109
+ console.print(f" • [cyan]{tool['name']}[/cyan]: {tool['description']}")
110
+
111
+ console.print()
112
+ console.print("[bold]Next steps:[/bold]")
113
+ console.print(" 1. Configure your API key: [cyan]supyagent config set[/cyan]")
114
+ console.print(" 2. Create an agent: [cyan]supyagent new myagent[/cyan]")
115
+ console.print(" 3. Start chatting: [cyan]supyagent chat myagent[/cyan]")
116
+
117
+
46
118
  @cli.command()
47
119
  @click.argument("name")
48
120
  @click.option(
@@ -636,15 +708,14 @@ def run(
636
708
  try:
637
709
  config = load_agent_config(agent_name)
638
710
  except AgentNotFoundError as e:
639
- console.print(f"[red]Error:[/red] {e}", err=True)
711
+ console_err.print(f"[red]Error:[/red] {e}")
640
712
  sys.exit(1)
641
713
 
642
714
  # Warn if using interactive agent in execution mode
643
715
  if config.type != "execution" and not quiet:
644
- console.print(
716
+ console_err.print(
645
717
  f"[yellow]Note:[/yellow] '{agent_name}' is an interactive agent. "
646
- "Consider using 'chat' for interactive use.",
647
- err=True,
718
+ "Consider using 'chat' for interactive use."
648
719
  )
649
720
 
650
721
  # Parse secrets
@@ -659,9 +730,7 @@ def run(
659
730
  else:
660
731
  input_path = Path(input_file)
661
732
  if not input_path.exists():
662
- console.print(
663
- f"[red]Error:[/red] File not found: {input_file}", err=True
664
- )
733
+ console_err.print(f"[red]Error:[/red] File not found: {input_file}")
665
734
  sys.exit(1)
666
735
  task_content = input_path.read_text().strip()
667
736
  elif task:
@@ -675,22 +744,21 @@ def run(
675
744
  if not sys.stdin.isatty():
676
745
  task_content = sys.stdin.read().strip()
677
746
  else:
678
- console.print(
747
+ console_err.print(
679
748
  "[red]Error:[/red] No task provided. "
680
- "Use positional argument, --input, or pipe to stdin.",
681
- err=True,
749
+ "Use positional argument, --input, or pipe to stdin."
682
750
  )
683
751
  sys.exit(1)
684
752
 
685
753
  if not task_content:
686
- console.print("[red]Error:[/red] Empty task", err=True)
754
+ console_err.print("[red]Error:[/red] Empty task")
687
755
  sys.exit(1)
688
756
 
689
757
  # Run the agent
690
758
  runner = ExecutionRunner(config)
691
759
 
692
760
  if not quiet:
693
- console.print(f"[dim]Running {agent_name}...[/dim]", err=True)
761
+ console_err.print(f"[dim]Running {agent_name}...[/dim]")
694
762
 
695
763
  result = runner.run(task_content, secrets=secrets_dict, output_format=output_format)
696
764
 
@@ -700,7 +768,7 @@ def run(
700
768
  elif result["ok"]:
701
769
  click.echo(result["data"])
702
770
  else:
703
- console.print(f"[red]Error:[/red] {result['error']}", err=True)
771
+ console_err.print(f"[red]Error:[/red] {result['error']}")
704
772
  sys.exit(1)
705
773
 
706
774
 
@@ -756,7 +824,7 @@ def batch(
756
824
  try:
757
825
  config = load_agent_config(agent_name)
758
826
  except AgentNotFoundError as e:
759
- console.print(f"[red]Error:[/red] {e}", err=True)
827
+ console_err.print(f"[red]Error:[/red] {e}")
760
828
  sys.exit(1)
761
829
 
762
830
  # Parse secrets
supyagent/core/llm.py CHANGED
@@ -4,9 +4,13 @@ LiteLLM wrapper for unified LLM access.
4
4
 
5
5
  from typing import Any
6
6
 
7
+ import litellm
7
8
  from litellm import completion
8
9
  from litellm.types.utils import ModelResponse
9
10
 
11
+ # Suppress LiteLLM debug messages (e.g., "Provider List: ...")
12
+ litellm.suppress_debug_info = True
13
+
10
14
 
11
15
  class LLMClient:
12
16
  """
supyagent/core/tools.py CHANGED
@@ -193,43 +193,61 @@ def supypowers_to_openai_tools(sp_tools: list[dict[str, Any]]) -> list[dict[str,
193
193
  Convert supypowers tool definitions to OpenAI function calling format.
194
194
 
195
195
  Supypowers docs output format:
196
- {
197
- "script": "hello",
198
- "function": "hello",
199
- "description": "...",
200
- "input_schema": {...}
201
- }
196
+ [
197
+ {
198
+ "script": "/path/to/script.py",
199
+ "functions": [
200
+ {
201
+ "name": "function_name",
202
+ "description": "...",
203
+ "input_schema": {...}
204
+ }
205
+ ]
206
+ }
207
+ ]
202
208
 
203
209
  OpenAI format:
204
210
  {
205
211
  "type": "function",
206
212
  "function": {
207
- "name": "hello__hello",
213
+ "name": "script__function_name",
208
214
  "description": "...",
209
215
  "parameters": {...}
210
216
  }
211
217
  }
212
218
  """
219
+ import os
220
+
213
221
  openai_tools = []
214
222
 
215
- for sp_tool in sp_tools:
216
- script = sp_tool.get("script", "")
217
- func = sp_tool.get("function", "")
218
- description = sp_tool.get("description", "No description")
219
- input_schema = sp_tool.get("input_schema", {"type": "object", "properties": {}})
220
-
221
- # Use double underscore to join script:function (since : isn't allowed in function names)
222
- name = f"{script}__{func}"
223
-
224
- openai_tool = {
225
- "type": "function",
226
- "function": {
227
- "name": name,
228
- "description": description,
229
- "parameters": input_schema,
230
- },
231
- }
223
+ for script_entry in sp_tools:
224
+ script_path = script_entry.get("script", "")
225
+ functions = script_entry.get("functions", [])
226
+
227
+ # Extract script name from path (e.g., "files" from "/path/to/files.py")
228
+ script_name = os.path.splitext(os.path.basename(script_path))[0]
229
+
230
+ # Skip __init__ files with no functions
231
+ if script_name == "__init__" and not functions:
232
+ continue
233
+
234
+ for func_def in functions:
235
+ func_name = func_def.get("name", "")
236
+ description = func_def.get("description", "No description")
237
+ input_schema = func_def.get("input_schema", {"type": "object", "properties": {}})
238
+
239
+ # Use double underscore to join script:function (since : isn't allowed in function names)
240
+ name = f"{script_name}__{func_name}"
241
+
242
+ openai_tool = {
243
+ "type": "function",
244
+ "function": {
245
+ "name": name,
246
+ "description": description,
247
+ "parameters": input_schema,
248
+ },
249
+ }
232
250
 
233
- openai_tools.append(openai_tool)
251
+ openai_tools.append(openai_tool)
234
252
 
235
253
  return openai_tools
@@ -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.2
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=5YqOevjbDdGNhA_gIIqnSATWThxj87TB_59RHzyL5gQ,38434
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
@@ -9,17 +9,20 @@ supyagent/core/context.py,sha256=ywen6IjWxcudxtipeqA_PHpZKdjK52eqOb9xy7QKnTM,456
9
9
  supyagent/core/credentials.py,sha256=35JBrzMOEDzHnn3XuyGmRjXVdSICL3tups3BLO_NCXQ,7600
10
10
  supyagent/core/delegation.py,sha256=B6OYlLjkZaRTPMQsK9nIGeXVO9JeC7RDpxXsOk652xs,9391
11
11
  supyagent/core/executor.py,sha256=awJXseQKO0I-D-5YqGxJ9MEN1b2DTVk_8kOQZExFYhs,7893
12
- supyagent/core/llm.py,sha256=z16cjqjPfwRGtEpUhZe1XlzOXJdlgKHjckNdeJnEWYw,1898
12
+ supyagent/core/llm.py,sha256=WpGGYARvVdnMGfZXNTf99bY7UqGJLPz0hdlIR4bb9Nk,2012
13
13
  supyagent/core/registry.py,sha256=1CGCPsOcrIc_63_oJaFmHY8WwMgJEvbwsv8mnsOBWtY,7326
14
14
  supyagent/core/session_manager.py,sha256=ngqrbofR8V0wZSuOfGHTtSYE0C9U6noDrzfDsO8wi24,7389
15
- supyagent/core/tools.py,sha256=exIynhwulsMJTzzW5NmDqBeXepULAt4FlVhGbtrvdXE,6627
15
+ supyagent/core/tools.py,sha256=qpCKdP5DBYj_XlCvMTskgiMQcUYSRWbsTolKmjkBO3w,7289
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.2.dist-info/METADATA,sha256=Fa3A8EH-0m7jeG1fMcHMunVogRK9iS0VFMf_0pM6Vf8,10387
25
+ supyagent-0.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
26
+ supyagent-0.2.2.dist-info/entry_points.txt,sha256=7dFXWtRewhLfA5XUckV7Yu1E5_c6c9lq8Gz-7BFs6TM,53
27
+ supyagent-0.2.2.dist-info/licenses/LICENSE,sha256=35fw1cvTM-IhiR3xaUohbiFBc2OBNRjI5z1b1cF7vZI,1067
28
+ supyagent-0.2.2.dist-info/RECORD,,