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/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())
@@ -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.