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/loader.py ADDED
@@ -0,0 +1,217 @@
1
+ """skills/loader.py — discover and load skill modules.
2
+
3
+ A skill is a folder under `skills/` containing:
4
+ skill.py — must export SKILL, TOOL_SCHEMAS, TOOL_CALLABLES
5
+ and optionally COMMANDS, HELP
6
+ skill.md — optional metadata (frontmatter: name, description,
7
+ version, author) + free-form help body
8
+
9
+ Contract:
10
+ SKILL : dict {"name": str, "description": str}
11
+ TOOL_SCHEMAS : list[dict] OpenAI function-calling schemas
12
+ TOOL_CALLABLES : dict[str, fn] name -> python callable
13
+ COMMANDS : dict optional
14
+ {
15
+ "cmdname": {
16
+ "handler": fn(args_list, log_fn),
17
+ "description": str,
18
+ },
19
+ ...
20
+ }
21
+ HELP : str optional fallback help text
22
+
23
+ The loader never crashes on a bad skill; it prints a warning and skips it.
24
+ """
25
+
26
+ import re
27
+ import importlib.util
28
+ from pathlib import Path
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Metadata extraction (no code execution)
33
+ # ---------------------------------------------------------------------------
34
+
35
+ _FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n?", re.DOTALL)
36
+
37
+
38
+ def _read_skill_md(folder: Path) -> dict:
39
+ """Parse the optional skill.md frontmatter. Returns {} if missing/invalid."""
40
+ md = folder / "skill.md"
41
+ if not md.exists():
42
+ return {}
43
+ try:
44
+ text = md.read_text(encoding="utf-8", errors="replace")
45
+ except Exception:
46
+ return {}
47
+ m = _FRONTMATTER_RE.match(text)
48
+ if not m:
49
+ return {}
50
+ meta = {}
51
+ for line in m.group(1).splitlines():
52
+ if ":" not in line:
53
+ continue
54
+ k, _, v = line.partition(":")
55
+ k = k.strip().lower()
56
+ v = v.strip().strip('"').strip("'")
57
+ if k in ("name", "description", "version", "author"):
58
+ meta[k] = v
59
+ return meta
60
+
61
+
62
+ def _read_skill_md_body(folder: Path) -> str:
63
+ """Return the text after the closing frontmatter --- of skill.md."""
64
+ md = folder / "skill.md"
65
+ if not md.exists():
66
+ return ""
67
+ try:
68
+ text = md.read_text(encoding="utf-8", errors="replace")
69
+ except Exception:
70
+ return ""
71
+ m = _FRONTMATTER_RE.match(text)
72
+ if m:
73
+ return text[m.end():].strip()
74
+ return text.strip()
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Discovery — list skills without importing them
79
+ # ---------------------------------------------------------------------------
80
+
81
+ def discover_skills(skills_dir: Path) -> list:
82
+ """Return list of dicts for each folder containing skill.py.
83
+
84
+ Each dict: {name, description, version, author, path}
85
+ Metadata is taken from skill.md when present; otherwise from the folder name.
86
+ """
87
+ out = []
88
+ skills_dir = Path(skills_dir)
89
+ if not skills_dir.exists():
90
+ return out
91
+
92
+ for folder in sorted(skills_dir.iterdir()):
93
+ if not folder.is_dir():
94
+ continue
95
+ if not (folder / "skill.py").exists():
96
+ continue
97
+
98
+ meta = {
99
+ "name": folder.name,
100
+ "description": "",
101
+ "version": "?",
102
+ "author": "?",
103
+ "path": str(folder),
104
+ }
105
+ meta.update(_read_skill_md(folder))
106
+ out.append(meta)
107
+
108
+ return out
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Loading — import skill.py and extract the contract
113
+ # ---------------------------------------------------------------------------
114
+
115
+ def load_skills(skills_dir: Path, disabled: list) -> tuple:
116
+ """Import every enabled skill.py and collect its exports.
117
+
118
+ Returns:
119
+ schemas : list[dict] merged OpenAI tool schemas
120
+ callables : dict[str, fn] merged tool callables
121
+ metadata : list[dict] SKILL dicts of loaded skills
122
+ commands : dict merged COMMANDS
123
+ """
124
+ schemas, callables, metadata, commands = [], {}, [], {}
125
+ skills_dir = Path(skills_dir)
126
+ if not skills_dir.exists():
127
+ return schemas, callables, metadata, commands
128
+
129
+ disabled = set(disabled or [])
130
+
131
+ for skill_file in sorted(skills_dir.glob("*/skill.py")):
132
+ name = skill_file.parent.name
133
+
134
+ if name in disabled:
135
+ print(f"[loader] skipped (disabled): {name}")
136
+ continue
137
+
138
+ try:
139
+ spec = importlib.util.spec_from_file_location(f"skill_{name}", skill_file)
140
+ mod = importlib.util.module_from_spec(spec)
141
+ spec.loader.exec_module(mod)
142
+ except Exception as e:
143
+ print(f"[loader] FAILED to import '{name}': {e}")
144
+ continue
145
+
146
+ try:
147
+ schemas.extend(getattr(mod, "TOOL_SCHEMAS", []) or [])
148
+ callables.update(getattr(mod, "TOOL_CALLABLES", {}) or {})
149
+
150
+ cmds = getattr(mod, "COMMANDS", {}) or {}
151
+ for cname, cdef in cmds.items():
152
+ if not isinstance(cdef, dict) or "handler" not in cdef:
153
+ print(f"[loader] skipping malformed command '{cname}' in {name}")
154
+ continue
155
+ commands[cname] = cdef
156
+
157
+ meta = getattr(mod, "SKILL", {"name": name})
158
+ metadata.append(meta)
159
+ print(f"[loader] loaded skill: {meta.get('name', name)}")
160
+ except Exception as e:
161
+ print(f"[loader] FAILED to read contract of '{name}': {e}")
162
+
163
+ return schemas, callables, metadata, commands
164
+
165
+
166
+ def list_commands(commands: dict) -> str:
167
+ """Pretty-print a COMMANDS dict for logging."""
168
+ if not commands:
169
+ return "(no commands)"
170
+ lines = []
171
+ for name, d in sorted(commands.items()):
172
+ desc = d.get("description", "")
173
+ lines.append(f" /{name:<14} {desc}")
174
+ return "\n".join(lines)
175
+
176
+
177
+ # ---------------------------------------------------------------------------
178
+ # Per-skill help
179
+ # ---------------------------------------------------------------------------
180
+
181
+ def get_skill_help(skills_dir: Path, name: str) -> str:
182
+ """Return the help text for a skill.
183
+
184
+ Priority:
185
+ 1. skill.md body (text after the closing --- of the frontmatter)
186
+ 2. HELP string exported from skill.py
187
+ 3. SKILL["description"]
188
+ 4. "(no help available)"
189
+ """
190
+ skills_dir = Path(skills_dir)
191
+ folder = skills_dir / name
192
+ if not folder.is_dir():
193
+ return f"(skill '{name}' not found)"
194
+
195
+ # 1. skill.md body
196
+ body = _read_skill_md_body(folder)
197
+ if body:
198
+ return body
199
+
200
+ # 2. HELP export from skill.py
201
+ skill_py = folder / "skill.py"
202
+ if skill_py.exists():
203
+ try:
204
+ spec = importlib.util.spec_from_file_location(f"skill_{name}", skill_py)
205
+ mod = importlib.util.module_from_spec(spec)
206
+ spec.loader.exec_module(mod)
207
+ h = getattr(mod, "HELP", None)
208
+ if isinstance(h, str) and h.strip():
209
+ return h.strip()
210
+ meta = getattr(mod, "SKILL", {}) or {}
211
+ desc = meta.get("description", "")
212
+ if desc:
213
+ return f"# {meta.get('name', name)}\n\n{desc}"
214
+ except Exception as e:
215
+ return f"(failed to load skill help: {e})"
216
+
217
+ return "(no help available)"
agent_code/paths.py ADDED
@@ -0,0 +1,54 @@
1
+ """Path resolution for pip-installed use."""
2
+ import os, shutil
3
+ from pathlib import Path
4
+
5
+ PACKAGE_ROOT = Path(__file__).parent.resolve()
6
+ DATA_DIR = PACKAGE_ROOT / "data"
7
+ BUNDLED_SITE = DATA_DIR / "site"
8
+ BUNDLED_SKILLS = DATA_DIR / "skills"
9
+
10
+ def user_config_dir() -> Path:
11
+ if os.name == "nt":
12
+ base = os.environ.get("APPDATA") or str(Path.home())
13
+ p = Path(base) / "agent-code"
14
+ else:
15
+ xdg = os.environ.get("XDG_CONFIG_HOME")
16
+ base = Path(xdg) if xdg else (Path.home() / ".config")
17
+ p = base / "agent-code"
18
+ p.mkdir(parents=True, exist_ok=True)
19
+ return p
20
+
21
+ def user_config_file() -> Path:
22
+ return user_config_dir() / "agent_config.json"
23
+
24
+ def project_dir() -> Path: return Path.cwd()
25
+ def project_workspace_dir() -> Path:
26
+ p = project_dir() / "workspace"; p.mkdir(parents=True, exist_ok=True); return p
27
+ def project_skills_dir() -> Path:
28
+ p = project_dir() / "skills"; p.mkdir(parents=True, exist_ok=True); return p
29
+ def project_chats_dir() -> Path:
30
+ p = project_dir() / "chats"; p.mkdir(parents=True, exist_ok=True); return p
31
+
32
+ def project_config_file() -> Path:
33
+ local = project_dir() / "agent_config.json"
34
+ return local if local.exists() else user_config_file()
35
+
36
+ def list_bundled_skills() -> list:
37
+ if not BUNDLED_SKILLS.exists(): return []
38
+ return sorted(d.name for d in BUNDLED_SKILLS.iterdir()
39
+ if d.is_dir() and (d / "skill.py").exists())
40
+
41
+ def install_bundled_skills(overwrite: bool = False) -> list:
42
+ target = project_skills_dir(); written = []
43
+ if not BUNDLED_SKILLS.exists(): return written
44
+ for folder in sorted(BUNDLED_SKILLS.iterdir()):
45
+ if not folder.is_dir(): continue
46
+ if not (folder / "skill.py").exists(): continue
47
+ dst = target / folder.name
48
+ if dst.exists() and not overwrite: continue
49
+ if dst.exists(): shutil.rmtree(dst)
50
+ shutil.copytree(folder, dst); written.append(folder.name)
51
+ return written
52
+
53
+ def bundled_site_dir() -> Path:
54
+ return BUNDLED_SITE