agent-code 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.
- agent_code/__init__.py +2 -0
- agent_code/__main__.py +3 -0
- agent_code/agent.py +1542 -0
- agent_code/cli.py +69 -0
- agent_code/data/skills/filesystem/skill.md +0 -0
- agent_code/data/skills/filesystem/skill.py +133 -0
- agent_code/data/skills/public_api/skill.md +42 -0
- agent_code/data/skills/public_api/skill.py +105 -0
- agent_code/data/skills/temp_mail/skill.md +51 -0
- agent_code/data/skills/temp_mail/skill.py +716 -0
- agent_code/loader.py +217 -0
- agent_code/paths.py +54 -0
- agent_code/skill_manager.py +1171 -0
- agent_code-1.0.0.dist-info/METADATA +434 -0
- agent_code-1.0.0.dist-info/RECORD +20 -0
- agent_code-1.0.0.dist-info/WHEEL +5 -0
- agent_code-1.0.0.dist-info/entry_points.txt +2 -0
- agent_code-1.0.0.dist-info/licenses/LICENSE +21 -0
- agent_code-1.0.0.dist-info/licenses/LICENSE-GPL +674 -0
- agent_code-1.0.0.dist-info/top_level.txt +1 -0
agent_code/cli.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""CLI entry point. Subcommands: run, manage, serve, skills."""
|
|
2
|
+
import sys, argparse
|
|
3
|
+
from . import __version__
|
|
4
|
+
|
|
5
|
+
def main(argv=None):
|
|
6
|
+
ap = argparse.ArgumentParser(
|
|
7
|
+
prog="agent-code",
|
|
8
|
+
description="Local AI agent with headless browser and skills.")
|
|
9
|
+
ap.add_argument("--version", action="version",
|
|
10
|
+
version=f"agent-code {__version__}")
|
|
11
|
+
sub = ap.add_subparsers(dest="cmd")
|
|
12
|
+
|
|
13
|
+
sub.add_parser("run", help="run the agent (default)").set_defaults(func=_run)
|
|
14
|
+
sub.add_parser("manage", help="skill manager GUI").set_defaults(func=_manage)
|
|
15
|
+
|
|
16
|
+
p = sub.add_parser("serve", help="serve the landing page")
|
|
17
|
+
p.add_argument("--host", default=None)
|
|
18
|
+
p.add_argument("--port", type=int, default=None)
|
|
19
|
+
p.set_defaults(func=_serve)
|
|
20
|
+
|
|
21
|
+
p = sub.add_parser("skills", help="list or install bundled skills")
|
|
22
|
+
p.add_argument("action", nargs="?", default="list",
|
|
23
|
+
choices=["list", "install"])
|
|
24
|
+
p.set_defaults(func=_skills)
|
|
25
|
+
|
|
26
|
+
args = ap.parse_args(argv)
|
|
27
|
+
if args.cmd is None:
|
|
28
|
+
return _run(args)
|
|
29
|
+
return args.func(args)
|
|
30
|
+
|
|
31
|
+
def _run(args):
|
|
32
|
+
from .agent import main as m
|
|
33
|
+
m(); return 0
|
|
34
|
+
|
|
35
|
+
def _manage(args):
|
|
36
|
+
from .skill_manager import main as m
|
|
37
|
+
m(); return 0
|
|
38
|
+
|
|
39
|
+
def _serve(args):
|
|
40
|
+
try:
|
|
41
|
+
from .landing import main as m
|
|
42
|
+
except ImportError:
|
|
43
|
+
print("Landing page not bundled in this install.")
|
|
44
|
+
return 1
|
|
45
|
+
argv = []
|
|
46
|
+
if args.host: argv += ["--host", args.host]
|
|
47
|
+
if args.port: argv += ["--port", str(args.port)]
|
|
48
|
+
return m(argv) or 0
|
|
49
|
+
|
|
50
|
+
def _skills(args):
|
|
51
|
+
from .paths import (install_bundled_skills, list_bundled_skills,
|
|
52
|
+
project_skills_dir)
|
|
53
|
+
if args.action == "list":
|
|
54
|
+
print("Bundled skills:")
|
|
55
|
+
for n in list_bundled_skills():
|
|
56
|
+
print(f" - {n}")
|
|
57
|
+
print()
|
|
58
|
+
print(f"Install location: {project_skills_dir()}")
|
|
59
|
+
return 0
|
|
60
|
+
copied = install_bundled_skills(overwrite=False)
|
|
61
|
+
if not copied:
|
|
62
|
+
print("All bundled skills are already installed.")
|
|
63
|
+
else:
|
|
64
|
+
for name in copied:
|
|
65
|
+
print(f" installed: {name}")
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
sys.exit(main())
|
|
Binary file
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""filesystem skill — read/write files, confined to the workspace sandbox."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
SKILL = {
|
|
7
|
+
"name": "filesystem",
|
|
8
|
+
"description": (
|
|
9
|
+
"Create, read, and list files INSIDE the sandbox workspace. "
|
|
10
|
+
"write_file creates a file, append_file adds chunks, read_file "
|
|
11
|
+
"inspects, list_dir explores. Paths are relative to the workspace."
|
|
12
|
+
),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
WORKSPACE = Path(os.environ.get("AGENT_WORKSPACE", "workspace")).resolve()
|
|
16
|
+
WORKSPACE.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
MAX_READ = 20_000
|
|
19
|
+
MAX_WRITE_CHUNK = 200_000 # ~200 KB per call
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _safe(path: str) -> Path:
|
|
23
|
+
"""Resolve `path` under WORKSPACE; reject anything that escapes."""
|
|
24
|
+
if not isinstance(path, str):
|
|
25
|
+
raise ValueError("path must be a string")
|
|
26
|
+
if os.path.isabs(path):
|
|
27
|
+
p = Path(path).resolve()
|
|
28
|
+
if not str(p).startswith(str(WORKSPACE)):
|
|
29
|
+
raise ValueError(f"absolute path outside workspace: {path}")
|
|
30
|
+
return p
|
|
31
|
+
p = (WORKSPACE / path).resolve()
|
|
32
|
+
if not str(p).startswith(str(WORKSPACE)):
|
|
33
|
+
raise ValueError(f"path escapes workspace: {path}")
|
|
34
|
+
return p
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def write_file(path: str, content: str) -> str:
|
|
38
|
+
"""Create or overwrite a file. Content capped at MAX_WRITE_CHUNK."""
|
|
39
|
+
try:
|
|
40
|
+
if not isinstance(content, str):
|
|
41
|
+
return "ERROR: content must be a string"
|
|
42
|
+
if len(content) > MAX_WRITE_CHUNK:
|
|
43
|
+
return (f"ERROR: content too large ({len(content)} chars, "
|
|
44
|
+
f"max {MAX_WRITE_CHUNK}). Write in chunks with write_file "
|
|
45
|
+
f"then append_file.")
|
|
46
|
+
p = _safe(path)
|
|
47
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
p.write_text(content, encoding="utf-8")
|
|
49
|
+
return f"OK wrote {len(content)} chars to {path} (total {p.stat().st_size} bytes)"
|
|
50
|
+
except Exception as e:
|
|
51
|
+
return f"ERROR: {e}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def append_file(path: str, content: str) -> str:
|
|
55
|
+
"""Append text to a file (creates it if missing)."""
|
|
56
|
+
try:
|
|
57
|
+
if not isinstance(content, str):
|
|
58
|
+
return "ERROR: content must be a string"
|
|
59
|
+
if len(content) > MAX_WRITE_CHUNK:
|
|
60
|
+
return f"ERROR: chunk too large ({len(content)} chars)"
|
|
61
|
+
p = _safe(path)
|
|
62
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
with p.open("a", encoding="utf-8") as f:
|
|
64
|
+
f.write(content)
|
|
65
|
+
return f"OK appended {len(content)} chars; file is now {p.stat().st_size} bytes"
|
|
66
|
+
except Exception as e:
|
|
67
|
+
return f"ERROR: {e}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def read_file(path: str) -> str:
|
|
71
|
+
try:
|
|
72
|
+
p = _safe(path)
|
|
73
|
+
if not p.exists():
|
|
74
|
+
return f"ERROR: not found: {path}"
|
|
75
|
+
return p.read_text(encoding="utf-8", errors="replace")[:MAX_READ]
|
|
76
|
+
except Exception as e:
|
|
77
|
+
return f"ERROR: {e}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def list_dir(path: str = ".") -> str:
|
|
81
|
+
try:
|
|
82
|
+
p = _safe(path)
|
|
83
|
+
if not p.is_dir():
|
|
84
|
+
return f"ERROR: not a directory: {path}"
|
|
85
|
+
rows = []
|
|
86
|
+
for child in sorted(p.iterdir()):
|
|
87
|
+
kind = "DIR " if child.is_dir() else "FILE"
|
|
88
|
+
size = child.stat().st_size if child.is_file() else 0
|
|
89
|
+
rows.append(f"{kind} {size:>9} {child.name}")
|
|
90
|
+
return "\n".join(rows) or "(empty)"
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return f"ERROR: {e}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
TOOL_SCHEMAS = [
|
|
96
|
+
{"type": "function", "function": {
|
|
97
|
+
"name": "write_file",
|
|
98
|
+
"description": (
|
|
99
|
+
"Create or overwrite a file in the workspace. "
|
|
100
|
+
"For large files, write the first chunk then use append_file."
|
|
101
|
+
),
|
|
102
|
+
"parameters": {"type": "object",
|
|
103
|
+
"properties": {
|
|
104
|
+
"path": {"type": "string",
|
|
105
|
+
"description": "Relative path, e.g. 'gta.html'"},
|
|
106
|
+
"content": {"type": "string"}},
|
|
107
|
+
"required": ["path", "content"]}}},
|
|
108
|
+
{"type": "function", "function": {
|
|
109
|
+
"name": "append_file",
|
|
110
|
+
"description": "Append text to a file in the workspace (creates if missing).",
|
|
111
|
+
"parameters": {"type": "object",
|
|
112
|
+
"properties": {"path": {"type": "string"},
|
|
113
|
+
"content": {"type": "string"}},
|
|
114
|
+
"required": ["path", "content"]}}},
|
|
115
|
+
{"type": "function", "function": {
|
|
116
|
+
"name": "read_file",
|
|
117
|
+
"description": "Read a file's text content from the workspace.",
|
|
118
|
+
"parameters": {"type": "object",
|
|
119
|
+
"properties": {"path": {"type": "string"}},
|
|
120
|
+
"required": ["path"]}}},
|
|
121
|
+
{"type": "function", "function": {
|
|
122
|
+
"name": "list_dir",
|
|
123
|
+
"description": "List files and folders in a workspace directory.",
|
|
124
|
+
"parameters": {"type": "object",
|
|
125
|
+
"properties": {"path": {"type": "string"}}}}},
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
TOOL_CALLABLES = {
|
|
129
|
+
"write_file": write_file,
|
|
130
|
+
"append_file": append_file,
|
|
131
|
+
"read_file": read_file,
|
|
132
|
+
"list_dir": list_dir,
|
|
133
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: public_api
|
|
3
|
+
description: Search free public APIs and call them.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
author: abdo
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# public_api
|
|
9
|
+
|
|
10
|
+
Search a catalog of 700+ free public APIs and call them directly.
|
|
11
|
+
|
|
12
|
+
## Source
|
|
13
|
+
|
|
14
|
+
https://public-api-lists.github.io/public-api-lists/api/all.json
|
|
15
|
+
|
|
16
|
+
Cached to %TEMP%/public_apis.json on first use. No auth, no
|
|
17
|
+
rate limit, 48 categories.
|
|
18
|
+
|
|
19
|
+
## Tools the LLM can call
|
|
20
|
+
|
|
21
|
+
search_apis(query, category, no_auth_only, limit)
|
|
22
|
+
Find APIs matching a keyword or category.
|
|
23
|
+
no_auth_only=True skips ones that need an API key.
|
|
24
|
+
|
|
25
|
+
call_api(url, max_chars)
|
|
26
|
+
GET a URL and return the first N chars of the body.
|
|
27
|
+
Use for raw JSON endpoints only.
|
|
28
|
+
|
|
29
|
+
## Example
|
|
30
|
+
|
|
31
|
+
User: find a free API for cat facts and call it
|
|
32
|
+
Agent:
|
|
33
|
+
search_apis(query="cat facts")
|
|
34
|
+
→ catfact.ninja | auth=No | https://catfact.ninja/
|
|
35
|
+
call_api(url="https://catfact.ninja/fact")
|
|
36
|
+
→ {"fact": "An adult lion's roar..."}
|
|
37
|
+
|
|
38
|
+
## Rules the agent follows
|
|
39
|
+
|
|
40
|
+
• Prefer APIs with auth == "No"
|
|
41
|
+
• Never fabricate URLs — only use ones returned by search
|
|
42
|
+
• Use the browser for HTML pages, call_api for raw JSON
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""public_api skill — search free public APIs and call them."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
SKILL = {
|
|
8
|
+
"name": "public_api",
|
|
9
|
+
"description": (
|
|
10
|
+
"Search free public APIs (weather, crypto, news, animals, etc.) from "
|
|
11
|
+
"the public-api-lists repo (730+ APIs, 48 categories) and call them."
|
|
12
|
+
),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
_CATALOG = Path(os.environ.get("TEMP", "/tmp")) / "public_apis.json"
|
|
16
|
+
_CATALOG_URL = "https://public-api-lists.github.io/public-api-lists/api/all.json"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _extract_entries(data):
|
|
20
|
+
if isinstance(data, list):
|
|
21
|
+
return [e for e in data if isinstance(e, dict)]
|
|
22
|
+
if isinstance(data, dict):
|
|
23
|
+
for k in ("data", "apis", "entries", "items", "results"):
|
|
24
|
+
if k in data and isinstance(data[k], list):
|
|
25
|
+
return [e for e in data[k] if isinstance(e, dict)]
|
|
26
|
+
collected = []
|
|
27
|
+
for v in data.values():
|
|
28
|
+
if isinstance(v, list):
|
|
29
|
+
collected.extend(x for x in v if isinstance(x, dict))
|
|
30
|
+
return collected
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _catalog():
|
|
35
|
+
if not _CATALOG.exists() or _CATALOG.stat().st_size == 0:
|
|
36
|
+
import urllib.request
|
|
37
|
+
with urllib.request.urlopen(_CATALOG_URL, timeout=30) as r:
|
|
38
|
+
_CATALOG.write_bytes(r.read())
|
|
39
|
+
raw = json.loads(_CATALOG.read_text(encoding="utf-8"))
|
|
40
|
+
return _extract_entries(raw)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def search_apis(query="", category="", no_auth_only=True, limit=10):
|
|
44
|
+
try:
|
|
45
|
+
entries = _catalog()
|
|
46
|
+
except Exception as e:
|
|
47
|
+
return f"ERROR: {e}"
|
|
48
|
+
if not entries:
|
|
49
|
+
return "ERROR: catalog empty or unexpected shape"
|
|
50
|
+
|
|
51
|
+
q, c = (query or "").lower().strip(), (category or "").lower().strip()
|
|
52
|
+
hits = []
|
|
53
|
+
for e in entries:
|
|
54
|
+
auth = str(e.get("auth", "")).lower()
|
|
55
|
+
if no_auth_only and auth not in ("no", "", "none", "false"):
|
|
56
|
+
continue
|
|
57
|
+
if c and str(e.get("category", "")).lower() != c:
|
|
58
|
+
continue
|
|
59
|
+
if q and q not in str(e.get("name", "")).lower() \
|
|
60
|
+
and q not in str(e.get("description", "")).lower():
|
|
61
|
+
continue
|
|
62
|
+
hits.append(e)
|
|
63
|
+
if len(hits) >= limit:
|
|
64
|
+
break
|
|
65
|
+
if not hits:
|
|
66
|
+
return "No matches."
|
|
67
|
+
return "\n".join(
|
|
68
|
+
f"{e.get('name','?')} | auth={e.get('auth','?')} | {e.get('link','?')}"
|
|
69
|
+
for e in hits
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def call_api(url, max_chars=4000):
|
|
74
|
+
if not url.startswith(("http://", "https://")):
|
|
75
|
+
return "ERROR: bad url"
|
|
76
|
+
try:
|
|
77
|
+
import urllib.request
|
|
78
|
+
req = urllib.request.Request(url, headers={"User-Agent": "agent/1.0"})
|
|
79
|
+
with urllib.request.urlopen(req, timeout=20) as r:
|
|
80
|
+
return r.read().decode("utf-8", errors="replace")[:max_chars] or "(empty)"
|
|
81
|
+
except Exception as e:
|
|
82
|
+
return f"ERROR: {e}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
TOOL_SCHEMAS = [
|
|
86
|
+
{"type": "function", "function": {
|
|
87
|
+
"name": "search_apis",
|
|
88
|
+
"description": "Search free public APIs.",
|
|
89
|
+
"parameters": {"type": "object", "properties": {
|
|
90
|
+
"query": {"type": "string"},
|
|
91
|
+
"category": {"type": "string"},
|
|
92
|
+
"no_auth_only": {"type": "boolean"},
|
|
93
|
+
"limit": {"type": "integer"}}}}},
|
|
94
|
+
{"type": "function", "function": {
|
|
95
|
+
"name": "call_api",
|
|
96
|
+
"description": "GET a public API URL and return the body.",
|
|
97
|
+
"parameters": {"type": "object",
|
|
98
|
+
"properties": {"url": {"type": "string"}},
|
|
99
|
+
"required": ["url"]}}},
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
TOOL_CALLABLES = {
|
|
103
|
+
"search_apis": search_apis,
|
|
104
|
+
"call_api": call_api,
|
|
105
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: temp_mail
|
|
3
|
+
description: Temporary email with multi-provider send and receive.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
author: abdo
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# temp_mail
|
|
9
|
+
|
|
10
|
+
Temporary email that can both receive and send.
|
|
11
|
+
|
|
12
|
+
## Receive chain (tries in order)
|
|
13
|
+
|
|
14
|
+
mail.tm REST API, no signup, no captcha
|
|
15
|
+
Guerrilla Mail AJAX, receive only
|
|
16
|
+
PhoboMail MCP-based, send + receive
|
|
17
|
+
|
|
18
|
+
## Send (configure one with /email)
|
|
19
|
+
|
|
20
|
+
Resend 100/day, 3000/mo https://resend.com
|
|
21
|
+
Brevo 300/day, 9000/mo https://brevo.com
|
|
22
|
+
Mailjet 200/day, 6000/mo https://mailjet.com
|
|
23
|
+
|
|
24
|
+
The temp inbox address is attached as Reply-To on every send,
|
|
25
|
+
so replies land back in check_temp_inbox().
|
|
26
|
+
|
|
27
|
+
## Tools the LLM can call
|
|
28
|
+
|
|
29
|
+
get_temp_email create or return the temp address
|
|
30
|
+
send_email send from the temp address
|
|
31
|
+
check_temp_inbox list received messages
|
|
32
|
+
read_temp_email read one by ID
|
|
33
|
+
reset_temp_email rotate to a fresh address
|
|
34
|
+
list_providers show all send + receive providers
|
|
35
|
+
|
|
36
|
+
## Commands you can type
|
|
37
|
+
|
|
38
|
+
/email show current status
|
|
39
|
+
/email resend <key>
|
|
40
|
+
/email brevo <key> <from@addr>
|
|
41
|
+
/email mailjet <user> <secret> <from@addr>
|
|
42
|
+
/providers list all providers
|
|
43
|
+
|
|
44
|
+
## Setup (30 seconds)
|
|
45
|
+
|
|
46
|
+
1. Sign up free at https://resend.com (no card)
|
|
47
|
+
2. Copy your API key (starts with re_)
|
|
48
|
+
3. In chat: /email resend re_YOUR_KEY
|
|
49
|
+
4. Try: send hi to someone@example.com
|
|
50
|
+
|
|
51
|
+
Do NOT retry send_email before /email is configured.
|