makefile-agent 0.4.2__tar.gz → 0.4.3__tar.gz

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.
Files changed (60) hide show
  1. {makefile_agent-0.4.2/makefile_agent.egg-info → makefile_agent-0.4.3}/PKG-INFO +1 -1
  2. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/builtin_tools/skill_tools.py +19 -3
  3. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/main.py +43 -3
  4. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/skill_backend.py +5 -1
  5. {makefile_agent-0.4.2 → makefile_agent-0.4.3/makefile_agent.egg-info}/PKG-INFO +1 -1
  6. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/makefile_agent.egg-info/SOURCES.txt +1 -0
  7. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/pyproject.toml +1 -1
  8. makefile_agent-0.4.3/tests/test_enabled_skills.py +210 -0
  9. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_main.py +1 -0
  10. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/LICENSE +0 -0
  11. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/README.md +0 -0
  12. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/__init__.py +0 -0
  13. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/agent_core/__init__.py +0 -0
  14. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/agent_core/agent.py +0 -0
  15. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/agent_core/bridge.py +0 -0
  16. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/agent_core/constants.py +0 -0
  17. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/agent_core/events.py +0 -0
  18. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/agent_core/export.py +0 -0
  19. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/agent_core/loop.py +0 -0
  20. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/agent_core/middleware.py +0 -0
  21. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/agent_core/provider.py +0 -0
  22. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/agent_shell/__init__.py +0 -0
  23. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/agent_shell/run.py +0 -0
  24. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/agent_shell/shell.py +0 -0
  25. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/agent_shell/user_messages.py +0 -0
  26. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/app_dirs.py +0 -0
  27. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/builtin_tools/__init__.py +0 -0
  28. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/builtin_tools/file_tools.py +0 -0
  29. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/memory/__init__.py +0 -0
  30. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/memory/memory.py +0 -0
  31. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/memory/tools.py +0 -0
  32. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/parser.py +0 -0
  33. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/protocols.py +0 -0
  34. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/provider/__init__.py +0 -0
  35. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/provider/anthropic.py +0 -0
  36. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/provider/base.py +0 -0
  37. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/provider/openai.py +0 -0
  38. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/templates/makefile/SYSTEM.md +0 -0
  39. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/tool_display.py +0 -0
  40. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/tool_handler/__init__.py +0 -0
  41. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/tool_handler/handler.py +0 -0
  42. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/make_agent/tool_handler/runner.py +0 -0
  43. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/makefile_agent.egg-info/dependency_links.txt +0 -0
  44. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/makefile_agent.egg-info/entry_points.txt +0 -0
  45. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/makefile_agent.egg-info/requires.txt +0 -0
  46. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/makefile_agent.egg-info/top_level.txt +0 -0
  47. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/setup.cfg +0 -0
  48. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_agent.py +0 -0
  49. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_agent_shell.py +0 -0
  50. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_app_dirs.py +0 -0
  51. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_bridge.py +0 -0
  52. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_builtin_tools.py +0 -0
  53. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_commands.py +0 -0
  54. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_compact.py +0 -0
  55. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_memory.py +0 -0
  56. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_middleware.py +0 -0
  57. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_parser.py +0 -0
  58. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_tools.py +0 -0
  59. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_trusted_skill.py +0 -0
  60. {makefile_agent-0.4.2 → makefile_agent-0.4.3}/tests/test_user_messages.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: makefile-agent
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: An AI agent powered by Makefile skills.
5
5
  Author: Dmitriy Sorochenkov
6
6
  License-Expression: MIT
@@ -120,16 +120,31 @@ def _skill_description(mk_path: Path) -> str:
120
120
  return " (no description)"
121
121
 
122
122
 
123
- def list_skills(skills_dir: str) -> str:
124
- """List all available skills with their names and descriptions."""
123
+ def list_skills(skills_dir: str, enabled_skills: frozenset[str] | None = None) -> str:
124
+ """List all available skills with their names and descriptions.
125
+
126
+ If *enabled_skills* is provided and is not ``{"*"}`` or empty, only those
127
+ skill names are shown. When the set contains ``"*"`` or is None, all
128
+ discovered skills are listed (default: show everything).
129
+ """
125
130
  path = Path(skills_dir)
126
131
  if not path.exists():
127
132
  return "No skills found (directory does not exist)"
128
- skill_dirs = sorted(
133
+
134
+ all_skill_dirs = sorted(
129
135
  p for p in path.iterdir() if p.is_dir() and (p / "skill.mk").exists()
130
136
  )
137
+ if not all_skill_dirs:
138
+ return "No skills found"
139
+
140
+ if enabled_skills is not None:
141
+ skill_dirs = [sd for sd in all_skill_dirs if sd.name in enabled_skills]
142
+ else:
143
+ skill_dirs = all_skill_dirs
144
+
131
145
  if not skill_dirs:
132
146
  return "No skills found"
147
+
133
148
  entries = []
134
149
  for sd in skill_dirs:
135
150
  desc = _skill_description(sd / "skill.mk")
@@ -168,6 +183,7 @@ def execute_skill(
168
183
  injected as environment variables; tokens after ``make`` are passed as make
169
184
  arguments (targets and/or make-style variable assignments).
170
185
  """
186
+
171
187
  if not _valid_skill_name(name):
172
188
  return f"Error: invalid skill name {name!r}. Use letters, numbers, hyphens, underscores, and dots only."
173
189
  safe_paths = _resolve_safe_skill_path(skills_dir, name, "skill.mk")
@@ -10,6 +10,7 @@ from make_agent.agent_core import (
10
10
  DEFAULT_COMPACT_MODE,
11
11
  DEFAULT_MAX_TOKENS,
12
12
  DEFAULT_MAX_TOOL_OUTPUT,
13
+ DEFAULT_TOOL_TIMEOUT,
13
14
  DEFAULT_USE_PROMPT_CACHE,
14
15
  )
15
16
  from make_agent.agent_shell import run
@@ -94,8 +95,36 @@ def _parse_trusted_skills(value: str | None) -> frozenset[str]:
94
95
  return frozenset(name.strip() for name in value.split(",") if name.strip())
95
96
 
96
97
 
97
- def _build_backend(skill_mode: str, skills_dir: str, tool_timeout: int):
98
- return MakefileSkillBackend(skills_dir, tool_timeout, Path.cwd())
98
+ def _parse_enabled_skills(
99
+ value: str | None, all_names: frozenset[str]
100
+ ) -> frozenset[str] | None:
101
+ """Parse --enabled-skills into a frozenset.
102
+
103
+ Returns None when the user didn't pass the flag (meaning: use all discovered skills).
104
+ When the flag is passed, returns the parsed set. 'all' maps to * (keep all).
105
+ Raises sys.exit on unknown skill names.
106
+ """
107
+ if not value:
108
+ return None
109
+
110
+ names = frozenset(name.strip() for name in value.split(",") if name.strip())
111
+ unknown = names - all_names
112
+ if unknown:
113
+ sys.exit(
114
+ "make-agent: unknown skill(s): "
115
+ f"{', '.join(sorted(unknown))}. Valid names: {', '.join(sorted(all_names))}"
116
+ )
117
+ return names
118
+
119
+
120
+ def _discover_skill_names(skills_dir: str) -> list[str]:
121
+ """Return a sorted list of all discoverable skill names from *skills_dir*."""
122
+ path = Path(skills_dir)
123
+ if not path.exists():
124
+ return []
125
+ return sorted(
126
+ p.name for p in path.iterdir() if p.is_dir() and (p / "skill.mk").exists()
127
+ )
99
128
 
100
129
 
101
130
  def _cmd_run(args: argparse.Namespace) -> None:
@@ -117,8 +146,13 @@ def _cmd_run(args: argparse.Namespace) -> None:
117
146
  else:
118
147
  skills_dir = str(default_skills_dir(_SKILL_MODE))
119
148
 
149
+ all_names = _discover_skill_names(skills_dir)
150
+ enabled_skills = _parse_enabled_skills(args.enabled_skills, frozenset(all_names))
151
+
120
152
  memory = Memory(mode_memory_path(_SKILL_MODE))
121
- backend = _build_backend(_SKILL_MODE, skills_dir, args.tool_timeout)
153
+ backend = MakefileSkillBackend(
154
+ skills_dir, DEFAULT_TOOL_TIMEOUT, Path.cwd(), enabled_skills
155
+ )
122
156
  trusted_skills = _parse_trusted_skills(getattr(args, "trusted_skills", None))
123
157
  tool_handler = ToolHandler(backend, memory, disabled, trusted_skills)
124
158
 
@@ -231,6 +265,12 @@ def main() -> None:
231
265
  metavar="EFFORT",
232
266
  help=f"Reasoning effort level ({'/'.join(_REASONING_EFFORT_VALUES)}, default: auto)",
233
267
  )
268
+ run_p.add_argument(
269
+ "--enabled-skills",
270
+ default=None,
271
+ metavar="SKILLS",
272
+ help="Comma-separated skill names to enable. By default all discovered skills are enabled.",
273
+ )
234
274
  run_p.add_argument(
235
275
  "--trusted-skills",
236
276
  default=None,
@@ -32,13 +32,17 @@ class MakefileSkillBackend:
32
32
  skills_dir: str,
33
33
  tool_timeout: int = 600,
34
34
  base_dir: Path | None = None,
35
+ enabled_skills: frozenset[str] | None = None,
35
36
  ) -> None:
36
37
  self._skills_dir = skills_dir
37
38
  self._tool_timeout = tool_timeout
38
39
  self._base_dir = base_dir if base_dir is not None else Path.cwd()
40
+ self._enabled_skills = enabled_skills
39
41
  self._schemas = MAKEFILE_SKILL_SCHEMAS + FILE_SCHEMAS
40
42
  self._executors: dict[str, Any] = {
41
- "list_skills": lambda **_kw: list_makefile_skills(self._skills_dir),
43
+ "list_skills": lambda **_kw: list_makefile_skills(
44
+ self._skills_dir, self._enabled_skills
45
+ ),
42
46
  "read_skill": lambda name, **_kw: read_makefile_skill(
43
47
  name, self._skills_dir
44
48
  ),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: makefile-agent
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: An AI agent powered by Makefile skills.
5
5
  Author: Dmitriy Sorochenkov
6
6
  License-Expression: MIT
@@ -48,6 +48,7 @@ tests/test_bridge.py
48
48
  tests/test_builtin_tools.py
49
49
  tests/test_commands.py
50
50
  tests/test_compact.py
51
+ tests/test_enabled_skills.py
51
52
  tests/test_main.py
52
53
  tests/test_memory.py
53
54
  tests/test_middleware.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "makefile-agent"
3
- version = "0.4.2"
3
+ version = "0.4.3"
4
4
  description = "An AI agent powered by Makefile skills."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1,210 @@
1
+ """Tests for the --enabled-skills CLI feature."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from unittest.mock import patch
8
+
9
+ import make_agent.main as main_module
10
+ from make_agent.builtin_tools.skill_tools import list_skills
11
+ from make_agent.main import _discover_skill_names
12
+ from make_agent.skill_backend import MakefileSkillBackend
13
+
14
+ _SKILL_MK = """\
15
+ define DESCRIPTION
16
+ A test skill
17
+ endef
18
+
19
+ .PHONY: greet
20
+ greet:
21
+ @echo hello
22
+ """
23
+
24
+
25
+ class TestParseEnabledSkills:
26
+ def test_none_returns_none(self):
27
+ assert main_module._parse_enabled_skills(None, frozenset()) is None
28
+
29
+ def test_comma_separated_list(self):
30
+ all_names = frozenset({"git", "run", "file-list"})
31
+ result = main_module._parse_enabled_skills("git,run", all_names)
32
+ assert result == frozenset({"git", "run"})
33
+
34
+ def test_single_name(self):
35
+ result = main_module._parse_enabled_skills("git", frozenset({"git"}))
36
+ assert result == frozenset({"git"})
37
+
38
+ def test_unknown_name_exits(self):
39
+ all_names = frozenset({"git", "run"})
40
+ with patch.object(sys, "exit", side_effect=SystemExit) as mock_exit:
41
+ try:
42
+ main_module._parse_enabled_skills("ghost", all_names)
43
+ except SystemExit:
44
+ pass
45
+ mock_exit.assert_called_once()
46
+
47
+ def test_whitespace_stripped(self):
48
+ result = main_module._parse_enabled_skills(
49
+ " git , run ", frozenset({"git", "run"})
50
+ )
51
+ assert result == frozenset({"git", "run"})
52
+
53
+
54
+ class TestDiscoverSkillNames:
55
+ def test_missing_dir(self, tmp_path):
56
+ assert _discover_skill_names(str(tmp_path / "nonexistent")) == []
57
+
58
+ def test_empty_dir(self, tmp_path):
59
+ assert _discover_skill_names(str(tmp_path)) == []
60
+
61
+ def test_returns_sorted_names(self, tmp_path):
62
+ (tmp_path / "zzz").mkdir()
63
+ (tmp_path / "zzz" / "skill.mk").write_text(_SKILL_MK)
64
+ (tmp_path / "aaa").mkdir()
65
+ (tmp_path / "aaa" / "skill.mk").write_text(_SKILL_MK)
66
+ assert _discover_skill_names(str(tmp_path)) == ["aaa", "zzz"]
67
+
68
+
69
+ class TestListSkillsFiltering:
70
+ def test_no_filter_shows_all(self, tmp_path):
71
+ for name in ("git", "run", "file-list"):
72
+ (tmp_path / name).mkdir()
73
+ (tmp_path / name / "skill.mk").write_text(_SKILL_MK)
74
+ result = list_skills(str(tmp_path))
75
+ assert "git:" in result
76
+ assert "run:" in result
77
+ assert "file-list:" in result
78
+
79
+ def test_subset_filters_correctly(self, tmp_path):
80
+ for name in ("git", "run", "file-list"):
81
+ (tmp_path / name).mkdir()
82
+ (tmp_path / name / "skill.mk").write_text(_SKILL_MK)
83
+ result = list_skills(
84
+ str(tmp_path), enabled_skills=frozenset({"git", "file-list"})
85
+ )
86
+ assert "git:" in result
87
+ assert "file-list:" in result
88
+ assert "run:" not in result
89
+
90
+ def test_empty_result_when_none_match(self, tmp_path):
91
+ (tmp_path / "git").mkdir()
92
+ (tmp_path / "git" / "skill.mk").write_text(_SKILL_MK)
93
+ result = list_skills(str(tmp_path), enabled_skills=frozenset({"ghost"}))
94
+ assert "No skills found" in result
95
+
96
+
97
+ class TestBackendIntegration:
98
+ def test_list_skills_filtered(self, tmp_path):
99
+ for name in ("git", "run"):
100
+ (tmp_path / name).mkdir()
101
+ (tmp_path / name / "skill.mk").write_text(_SKILL_MK)
102
+ backend = MakefileSkillBackend(str(tmp_path), enabled_skills=frozenset({"git"}))
103
+ result = backend.executors["list_skills"]()
104
+ assert "git:" in result
105
+ assert "run:" not in result
106
+
107
+ def test_default_no_filter(self, tmp_path):
108
+ """When enabled_skills is None (default), all skills work."""
109
+ (tmp_path / "git").mkdir()
110
+ (tmp_path / "git" / "skill.mk").write_text(_SKILL_MK)
111
+ backend = MakefileSkillBackend(str(tmp_path)) # no enabled_skills
112
+ result = backend.executors["list_skills"]()
113
+ assert "git:" in result
114
+
115
+
116
+ class TestCmdRunWithEnabledSkills:
117
+ def test_enabled_skills_passed_to_backend(self, tmp_path):
118
+ # _cmd_run appends /makefile to the skills_dir, so create there.
119
+ skills_dir = tmp_path / "skills" / "makefile"
120
+ skills_dir.mkdir(parents=True)
121
+ (skills_dir / "git").mkdir()
122
+ (skills_dir / "git" / "skill.mk").write_text(_SKILL_MK)
123
+ (skills_dir / "run").mkdir()
124
+ (skills_dir / "run" / "skill.mk").write_text(_SKILL_MK)
125
+
126
+ args = _run_args(
127
+ prompt="do it",
128
+ skills_dir=str(tmp_path / "skills"),
129
+ enabled_skills="git",
130
+ )
131
+ captured: dict = {}
132
+
133
+ async def _fake_run(**kwargs):
134
+ captured.update(kwargs)
135
+
136
+ with (
137
+ patch.object(main_module, "run", _fake_run),
138
+ patch.object(main_module, "ensure_mode_system_prompt"),
139
+ patch.object(
140
+ main_module, "mode_dir", return_value=tmp_path / "makefile-mode"
141
+ ),
142
+ patch.object(
143
+ main_module,
144
+ "mode_memory_path",
145
+ return_value=tmp_path / "makefile-memory.db",
146
+ ),
147
+ ):
148
+ main_module._cmd_run(args)
149
+
150
+ backend = captured["tool_handler"]._backend
151
+ assert isinstance(backend, MakefileSkillBackend)
152
+ result = backend.executors["list_skills"]()
153
+ assert "git:" in result
154
+ assert "run:" not in result
155
+
156
+ def test_no_enabled_skills_flag_passes_none(self, tmp_path):
157
+ # _cmd_run appends /makefile to the skills_dir, so create there.
158
+ skills_dir = tmp_path / "skills" / "makefile"
159
+ skills_dir.mkdir(parents=True)
160
+ (skills_dir / "git").mkdir()
161
+ (skills_dir / "git" / "skill.mk").write_text(_SKILL_MK)
162
+
163
+ args = _run_args(prompt="do it", skills_dir=str(tmp_path / "skills"))
164
+ captured: dict = {}
165
+
166
+ async def _fake_run(**kwargs):
167
+ captured.update(kwargs)
168
+
169
+ with (
170
+ patch.object(main_module, "run", _fake_run),
171
+ patch.object(main_module, "ensure_mode_system_prompt"),
172
+ patch.object(
173
+ main_module, "mode_dir", return_value=tmp_path / "makefile-mode"
174
+ ),
175
+ patch.object(
176
+ main_module,
177
+ "mode_memory_path",
178
+ return_value=tmp_path / "makefile-memory.db",
179
+ ),
180
+ ):
181
+ main_module._cmd_run(args)
182
+
183
+ backend = captured["tool_handler"]._backend
184
+ assert isinstance(backend, MakefileSkillBackend)
185
+ # None means all skills enabled
186
+ result = backend.executors["list_skills"]()
187
+ assert "git:" in result
188
+
189
+
190
+ def _run_args(**kwargs) -> argparse.Namespace:
191
+ defaults = dict(
192
+ model="model-x",
193
+ prompt=None,
194
+ prompt_file=None,
195
+ system=None,
196
+ system_file=None,
197
+ max_retries=5,
198
+ tool_timeout=600,
199
+ max_tool_output=20000,
200
+ max_tokens=4096,
201
+ skills_dir=None,
202
+ disable_builtin_tools=None,
203
+ reasoning_effort="auto",
204
+ skill_mode="makefile",
205
+ prompt_cache=False,
206
+ compact_mode="drop",
207
+ enabled_skills=None,
208
+ )
209
+ defaults.update(kwargs)
210
+ return argparse.Namespace(**defaults)
@@ -44,6 +44,7 @@ def _run_args(**kwargs) -> argparse.Namespace:
44
44
  skill_mode="makefile", # noqa: S106 - kept for test compatibility
45
45
  prompt_cache=False,
46
46
  compact_mode="drop",
47
+ enabled_skills=None,
47
48
  )
48
49
  defaults.update(kwargs)
49
50
  return argparse.Namespace(**defaults)
File without changes
File without changes
File without changes