agencode 0.1.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.
- agencli/__init__.py +5 -0
- agencli/__main__.py +9 -0
- agencli/agents/__init__.py +1 -0
- agencli/agents/editor.py +110 -0
- agencli/agents/factory.py +335 -0
- agencli/agents/management_tools.py +277 -0
- agencli/agents/prebuilt/__init__.py +1 -0
- agencli/agents/prebuilt/catalog.py +66 -0
- agencli/agents/registry.py +50 -0
- agencli/agents/runtime.py +266 -0
- agencli/agents/supervisor.py +67 -0
- agencli/cli.py +561 -0
- agencli/core/__init__.py +1 -0
- agencli/core/config.py +179 -0
- agencli/core/keystore.py +14 -0
- agencli/core/logger.py +17 -0
- agencli/core/paths.py +37 -0
- agencli/core/session.py +513 -0
- agencli/mcp/__init__.py +1 -0
- agencli/mcp/client.py +33 -0
- agencli/mcp/config.py +99 -0
- agencli/providers/__init__.py +1 -0
- agencli/providers/model.py +180 -0
- agencli/skills/__init__.py +37 -0
- agencli/skills/cli_backend.py +446 -0
- agencli/skills/loader.py +77 -0
- agencli/skills/manager.py +153 -0
- agencli/tools/__init__.py +1 -0
- agencli/tools/mcp.py +106 -0
- agencli/tui/__init__.py +1 -0
- agencli/tui/app.py +4274 -0
- agencli/tui/commands.py +86 -0
- agencli/tui/screens.py +939 -0
- agencli/tui/trace.py +334 -0
- agencli/tui/voice.py +77 -0
- agencode-0.1.0.dist-info/METADATA +44 -0
- agencode-0.1.0.dist-info/RECORD +39 -0
- agencode-0.1.0.dist-info/WHEEL +4 -0
- agencode-0.1.0.dist-info/entry_points.txt +3 -0
agencli/skills/loader.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from agencli.skills.manager import (
|
|
6
|
+
SKILL_FILE_NAME,
|
|
7
|
+
installed_skills_root,
|
|
8
|
+
named_skill_source_root,
|
|
9
|
+
skills_dir_has_legacy_layout,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
_INSTALLED_ALIASES = {"*", "all", "installed", "local"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def resolve_skill_sources(skills: list[str], skills_dir: str | Path) -> list[str]:
|
|
16
|
+
base_dir = Path(skills_dir).expanduser().resolve()
|
|
17
|
+
resolved: list[str] = []
|
|
18
|
+
seen: set[str] = set()
|
|
19
|
+
|
|
20
|
+
def add(source: str) -> None:
|
|
21
|
+
if source not in seen:
|
|
22
|
+
seen.add(source)
|
|
23
|
+
resolved.append(source)
|
|
24
|
+
|
|
25
|
+
for raw in skills:
|
|
26
|
+
token = raw.strip()
|
|
27
|
+
if not token:
|
|
28
|
+
continue
|
|
29
|
+
lowered = token.lower()
|
|
30
|
+
if lowered in _INSTALLED_ALIASES:
|
|
31
|
+
installed_root = installed_skills_root(base_dir)
|
|
32
|
+
if installed_root.exists():
|
|
33
|
+
add(installed_root.as_posix())
|
|
34
|
+
if skills_dir_has_legacy_layout(base_dir):
|
|
35
|
+
add(base_dir.as_posix())
|
|
36
|
+
continue
|
|
37
|
+
if _is_backend_virtual_source(token):
|
|
38
|
+
add(_normalize_virtual_source(token))
|
|
39
|
+
continue
|
|
40
|
+
if not _looks_like_path(token):
|
|
41
|
+
named_root = named_skill_source_root(base_dir) / token
|
|
42
|
+
if named_root.exists():
|
|
43
|
+
add(named_root.resolve().as_posix())
|
|
44
|
+
continue
|
|
45
|
+
add(_resolve_filesystem_source(token))
|
|
46
|
+
|
|
47
|
+
return resolved
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _is_backend_virtual_source(value: str) -> bool:
|
|
51
|
+
return value.startswith("/") and ":" not in value and "\\" not in value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _looks_like_path(value: str) -> bool:
|
|
55
|
+
return ":" in value or "\\" in value or "/" in value or value.startswith(".")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _normalize_virtual_source(value: str) -> str:
|
|
59
|
+
normalized = value.rstrip("/")
|
|
60
|
+
return f"{normalized}/" if normalized else "/"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _resolve_filesystem_source(value: str) -> str:
|
|
64
|
+
path = Path(value).expanduser()
|
|
65
|
+
if not path.exists():
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Unknown skill source `{value}`. Use `installed`, an installed skill name, or a filesystem path."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
resolved = path.resolve()
|
|
71
|
+
if resolved.is_file():
|
|
72
|
+
if resolved.name != SKILL_FILE_NAME:
|
|
73
|
+
raise ValueError(f"Skill path `{value}` must point to a directory or `{SKILL_FILE_NAME}` file.")
|
|
74
|
+
return resolved.parent.parent.as_posix()
|
|
75
|
+
if (resolved / SKILL_FILE_NAME).exists():
|
|
76
|
+
return resolved.parent.as_posix()
|
|
77
|
+
return resolved.as_posix()
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
SKILL_FILE_NAME = "SKILL.md"
|
|
11
|
+
_INSTALLED_DIR_NAME = "installed"
|
|
12
|
+
_BY_NAME_DIR_NAME = "by-name"
|
|
13
|
+
_FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---\s*(?:\n|$)", re.DOTALL)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class InstalledSkill:
|
|
18
|
+
name: str
|
|
19
|
+
description: str
|
|
20
|
+
path: Path
|
|
21
|
+
allowed_tools: list[str] = field(default_factory=list)
|
|
22
|
+
license: str | None = None
|
|
23
|
+
compatibility: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def install_skill(source: str | Path, skills_dir: str | Path, *, overwrite: bool = False) -> InstalledSkill:
|
|
27
|
+
source_dir = _coerce_skill_directory(source)
|
|
28
|
+
skill = read_skill(source_dir)
|
|
29
|
+
base_dir = Path(skills_dir).expanduser().resolve()
|
|
30
|
+
installed_dir = installed_skills_root(base_dir)
|
|
31
|
+
named_dir = named_skill_source_root(base_dir)
|
|
32
|
+
installed_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
named_dir.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
canonical_target = installed_dir / skill.name
|
|
36
|
+
named_target = named_dir / skill.name / skill.name
|
|
37
|
+
_copy_skill_tree(source_dir, canonical_target, overwrite=overwrite)
|
|
38
|
+
_copy_skill_tree(source_dir, named_target, overwrite=overwrite)
|
|
39
|
+
return read_skill(canonical_target)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def list_installed_skills(skills_dir: str | Path) -> list[InstalledSkill]:
|
|
43
|
+
base_dir = Path(skills_dir).expanduser().resolve()
|
|
44
|
+
skills: dict[str, InstalledSkill] = {}
|
|
45
|
+
for skill_dir in _iter_installed_skill_dirs(base_dir):
|
|
46
|
+
skill = read_skill(skill_dir)
|
|
47
|
+
skills[skill.name] = skill
|
|
48
|
+
return [skills[name] for name in sorted(skills)]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def read_skill(path: str | Path) -> InstalledSkill:
|
|
52
|
+
skill_dir = _coerce_skill_directory(path)
|
|
53
|
+
skill_file = skill_dir / SKILL_FILE_NAME
|
|
54
|
+
raw = skill_file.read_text(encoding="utf-8")
|
|
55
|
+
metadata = _parse_frontmatter(raw, skill_file)
|
|
56
|
+
|
|
57
|
+
name = str(metadata.get("name", "")).strip() or skill_dir.name
|
|
58
|
+
description = str(metadata.get("description", "")).strip()
|
|
59
|
+
if not description:
|
|
60
|
+
raise ValueError(f"Skill `{skill_file}` is missing a description in YAML frontmatter.")
|
|
61
|
+
|
|
62
|
+
raw_tools = metadata.get("allowed-tools") or metadata.get("allowed_tools") or ""
|
|
63
|
+
if isinstance(raw_tools, str):
|
|
64
|
+
allowed_tools = [tool.strip(",") for tool in raw_tools.split() if tool.strip(",")]
|
|
65
|
+
elif isinstance(raw_tools, list):
|
|
66
|
+
allowed_tools = [str(tool).strip() for tool in raw_tools if str(tool).strip()]
|
|
67
|
+
else:
|
|
68
|
+
allowed_tools = []
|
|
69
|
+
|
|
70
|
+
return InstalledSkill(
|
|
71
|
+
name=name,
|
|
72
|
+
description=description,
|
|
73
|
+
path=skill_dir.resolve(),
|
|
74
|
+
allowed_tools=allowed_tools,
|
|
75
|
+
license=str(metadata.get("license", "")).strip() or None,
|
|
76
|
+
compatibility=str(metadata.get("compatibility", "")).strip() or None,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def installed_skills_root(skills_dir: str | Path) -> Path:
|
|
81
|
+
return Path(skills_dir).expanduser().resolve() / _INSTALLED_DIR_NAME
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def named_skill_source_root(skills_dir: str | Path) -> Path:
|
|
85
|
+
return Path(skills_dir).expanduser().resolve() / _BY_NAME_DIR_NAME
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def skills_dir_has_legacy_layout(skills_dir: str | Path) -> bool:
|
|
89
|
+
base_dir = Path(skills_dir).expanduser().resolve()
|
|
90
|
+
if not base_dir.exists():
|
|
91
|
+
return False
|
|
92
|
+
for child in base_dir.iterdir():
|
|
93
|
+
if not child.is_dir() or child.name in {_INSTALLED_DIR_NAME, _BY_NAME_DIR_NAME}:
|
|
94
|
+
continue
|
|
95
|
+
if (child / SKILL_FILE_NAME).exists():
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _iter_installed_skill_dirs(skills_dir: Path) -> list[Path]:
|
|
101
|
+
skill_dirs: dict[str, Path] = {}
|
|
102
|
+
|
|
103
|
+
installed_dir = installed_skills_root(skills_dir)
|
|
104
|
+
if installed_dir.exists():
|
|
105
|
+
for child in installed_dir.iterdir():
|
|
106
|
+
if child.is_dir() and (child / SKILL_FILE_NAME).exists():
|
|
107
|
+
skill_dirs[child.name] = child
|
|
108
|
+
|
|
109
|
+
if skills_dir.exists():
|
|
110
|
+
for child in skills_dir.iterdir():
|
|
111
|
+
if not child.is_dir() or child.name in {_INSTALLED_DIR_NAME, _BY_NAME_DIR_NAME}:
|
|
112
|
+
continue
|
|
113
|
+
if (child / SKILL_FILE_NAME).exists():
|
|
114
|
+
skill_dirs.setdefault(child.name, child)
|
|
115
|
+
|
|
116
|
+
return list(skill_dirs.values())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _coerce_skill_directory(path: str | Path) -> Path:
|
|
120
|
+
candidate = Path(path).expanduser().resolve()
|
|
121
|
+
if candidate.is_file():
|
|
122
|
+
if candidate.name != SKILL_FILE_NAME:
|
|
123
|
+
raise ValueError(f"Expected `{SKILL_FILE_NAME}` or a skill directory, got `{candidate}`.")
|
|
124
|
+
candidate = candidate.parent
|
|
125
|
+
if not candidate.is_dir():
|
|
126
|
+
raise ValueError(f"Skill source `{candidate}` is not a directory.")
|
|
127
|
+
if not (candidate / SKILL_FILE_NAME).exists():
|
|
128
|
+
raise ValueError(f"Skill directory `{candidate}` does not contain `{SKILL_FILE_NAME}`.")
|
|
129
|
+
return candidate
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _parse_frontmatter(content: str, skill_file: Path) -> dict:
|
|
133
|
+
normalized = content.replace("\r\n", "\n")
|
|
134
|
+
match = _FRONTMATTER_PATTERN.match(normalized)
|
|
135
|
+
if not match:
|
|
136
|
+
raise ValueError(f"Skill `{skill_file}` is missing YAML frontmatter.")
|
|
137
|
+
loaded = yaml.safe_load(match.group(1))
|
|
138
|
+
if not isinstance(loaded, dict):
|
|
139
|
+
raise ValueError(f"Skill `{skill_file}` has invalid YAML frontmatter.")
|
|
140
|
+
return loaded
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _copy_skill_tree(source_dir: Path, target_dir: Path, *, overwrite: bool) -> None:
|
|
144
|
+
source_dir = source_dir.resolve()
|
|
145
|
+
target_dir = target_dir.resolve()
|
|
146
|
+
if source_dir == target_dir:
|
|
147
|
+
return
|
|
148
|
+
if target_dir.exists():
|
|
149
|
+
if not overwrite:
|
|
150
|
+
raise FileExistsError(f"Skill destination `{target_dir}` already exists.")
|
|
151
|
+
shutil.rmtree(target_dir)
|
|
152
|
+
target_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
shutil.copytree(source_dir, target_dir)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tool loaders and local tool definitions for AgenCLI."""
|
agencli/tools/mcp.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
import logging
|
|
6
|
+
import shutil
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from agencli.mcp.client import MCPToolClient
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def load_mcp_tools_async(
|
|
15
|
+
mcp_config_path: str,
|
|
16
|
+
server_names: list[str] | None = None,
|
|
17
|
+
*,
|
|
18
|
+
diagnostics: list[str] | None = None,
|
|
19
|
+
) -> list[Any]:
|
|
20
|
+
client = MCPToolClient(mcp_config_path)
|
|
21
|
+
server_map = client.load_server_map()
|
|
22
|
+
requested_servers = server_names or list(server_map)
|
|
23
|
+
if not requested_servers:
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
resolved_tools: list[Any] = []
|
|
27
|
+
for server_name in requested_servers:
|
|
28
|
+
if server_name not in server_map:
|
|
29
|
+
continue
|
|
30
|
+
try:
|
|
31
|
+
resolved_tools.extend(await client.get_tools(server_names=[server_name]))
|
|
32
|
+
except Exception as exc:
|
|
33
|
+
message = _format_mcp_error(server_name, server_map[server_name], exc)
|
|
34
|
+
if diagnostics is not None:
|
|
35
|
+
if message not in diagnostics:
|
|
36
|
+
diagnostics.append(message)
|
|
37
|
+
else:
|
|
38
|
+
logger.warning(message)
|
|
39
|
+
return resolved_tools
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _run_tool_request(client: MCPToolClient, server_names: list[str] | None = None) -> list[Any]:
|
|
43
|
+
try:
|
|
44
|
+
asyncio.get_running_loop()
|
|
45
|
+
except RuntimeError:
|
|
46
|
+
return asyncio.run(client.get_tools(server_names=server_names))
|
|
47
|
+
|
|
48
|
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
49
|
+
future = executor.submit(asyncio.run, client.get_tools(server_names=server_names))
|
|
50
|
+
return future.result()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _format_mcp_error(server_name: str, server_config: dict[str, Any], exc: Exception) -> str:
|
|
54
|
+
command = server_config.get("command") or ""
|
|
55
|
+
if command.lower().startswith("npx") and shutil.which(command) is None and shutil.which(f"{command}.cmd") is None:
|
|
56
|
+
return (
|
|
57
|
+
f"Skipping MCP server `{server_name}` because `{command}` is not available. "
|
|
58
|
+
"Install Node.js/npm or update the MCP config."
|
|
59
|
+
)
|
|
60
|
+
if command.lower().startswith("npx") and isinstance(exc, OSError) and getattr(exc, "errno", None) == 9:
|
|
61
|
+
return (
|
|
62
|
+
f"Skipping MCP server `{server_name}` because Windows could not open `{command}` cleanly. "
|
|
63
|
+
"Install or repair Node.js/npm, or update the MCP config."
|
|
64
|
+
)
|
|
65
|
+
if isinstance(exc, FileNotFoundError):
|
|
66
|
+
if command.lower().startswith("npx"):
|
|
67
|
+
return (
|
|
68
|
+
f"Skipping MCP server `{server_name}` because `{command}` is not available. "
|
|
69
|
+
"Install Node.js/npm or update the MCP config."
|
|
70
|
+
)
|
|
71
|
+
if command:
|
|
72
|
+
return f"Skipping MCP server `{server_name}` because command `{command}` was not found."
|
|
73
|
+
return f"Skipping MCP server `{server_name}` because it could not start: {exc}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_mcp_tools(
|
|
77
|
+
mcp_config_path: str,
|
|
78
|
+
server_names: list[str] | None = None,
|
|
79
|
+
*,
|
|
80
|
+
diagnostics: list[str] | None = None,
|
|
81
|
+
) -> list[Any]:
|
|
82
|
+
try:
|
|
83
|
+
asyncio.get_running_loop()
|
|
84
|
+
except RuntimeError:
|
|
85
|
+
return asyncio.run(load_mcp_tools_async(mcp_config_path, server_names=server_names, diagnostics=diagnostics))
|
|
86
|
+
|
|
87
|
+
client = MCPToolClient(mcp_config_path)
|
|
88
|
+
server_map = client.load_server_map()
|
|
89
|
+
requested_servers = server_names or list(server_map)
|
|
90
|
+
if not requested_servers:
|
|
91
|
+
return []
|
|
92
|
+
|
|
93
|
+
resolved_tools: list[Any] = []
|
|
94
|
+
for server_name in requested_servers:
|
|
95
|
+
if server_name not in server_map:
|
|
96
|
+
continue
|
|
97
|
+
try:
|
|
98
|
+
resolved_tools.extend(_run_tool_request(client, server_names=[server_name]))
|
|
99
|
+
except Exception as exc:
|
|
100
|
+
message = _format_mcp_error(server_name, server_map[server_name], exc)
|
|
101
|
+
if diagnostics is not None:
|
|
102
|
+
if message not in diagnostics:
|
|
103
|
+
diagnostics.append(message)
|
|
104
|
+
else:
|
|
105
|
+
logger.warning(message)
|
|
106
|
+
return resolved_tools
|
agencli/tui/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Textual UI for AgenCLI."""
|