makefile-agent 0.4.1__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.1 → makefile_agent-0.4.3}/PKG-INFO +1 -1
  2. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/agent.py +35 -11
  3. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_shell/run.py +1 -0
  4. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_shell/shell.py +44 -13
  5. makefile_agent-0.4.3/make_agent/agent_shell/user_messages.py +110 -0
  6. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/builtin_tools/skill_tools.py +19 -3
  7. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/main.py +53 -6
  8. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/memory/memory.py +15 -0
  9. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/skill_backend.py +11 -3
  10. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/tool_handler/handler.py +20 -4
  11. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/makefile_agent.egg-info/PKG-INFO +1 -1
  12. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/makefile_agent.egg-info/SOURCES.txt +4 -1
  13. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/pyproject.toml +1 -1
  14. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_agent.py +71 -21
  15. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_agent_shell.py +10 -2
  16. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_bridge.py +38 -12
  17. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_builtin_tools.py +63 -16
  18. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_commands.py +2 -2
  19. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_compact.py +159 -75
  20. makefile_agent-0.4.3/tests/test_enabled_skills.py +210 -0
  21. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_main.py +32 -9
  22. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_memory.py +200 -114
  23. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_middleware.py +29 -7
  24. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_parser.py +54 -14
  25. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_tools.py +13 -2
  26. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_trusted_skill.py +28 -8
  27. makefile_agent-0.4.3/tests/test_user_messages.py +127 -0
  28. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/LICENSE +0 -0
  29. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/README.md +0 -0
  30. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/__init__.py +0 -0
  31. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/__init__.py +0 -0
  32. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/bridge.py +0 -0
  33. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/constants.py +0 -0
  34. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/events.py +0 -0
  35. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/export.py +0 -0
  36. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/loop.py +0 -0
  37. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/middleware.py +0 -0
  38. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/provider.py +0 -0
  39. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_shell/__init__.py +0 -0
  40. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/app_dirs.py +0 -0
  41. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/builtin_tools/__init__.py +0 -0
  42. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/builtin_tools/file_tools.py +0 -0
  43. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/memory/__init__.py +0 -0
  44. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/memory/tools.py +0 -0
  45. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/parser.py +0 -0
  46. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/protocols.py +0 -0
  47. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/provider/__init__.py +0 -0
  48. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/provider/anthropic.py +0 -0
  49. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/provider/base.py +0 -0
  50. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/provider/openai.py +0 -0
  51. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/templates/makefile/SYSTEM.md +0 -0
  52. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/tool_display.py +0 -0
  53. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/tool_handler/__init__.py +0 -0
  54. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/tool_handler/runner.py +0 -0
  55. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/makefile_agent.egg-info/dependency_links.txt +0 -0
  56. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/makefile_agent.egg-info/entry_points.txt +0 -0
  57. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/makefile_agent.egg-info/requires.txt +0 -0
  58. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/makefile_agent.egg-info/top_level.txt +0 -0
  59. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/setup.cfg +0 -0
  60. {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_app_dirs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: makefile-agent
3
- Version: 0.4.1
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
@@ -67,7 +67,9 @@ class AgentManager:
67
67
  middlewares: list[MiddlewareBase] | None = None,
68
68
  ) -> None:
69
69
  self._tool_handler = tool_handler
70
- self._middlewares: list[MiddlewareBase] = middlewares if middlewares is not None else []
70
+ self._middlewares: list[MiddlewareBase] = (
71
+ middlewares if middlewares is not None else []
72
+ )
71
73
  self._sessions: dict[str, AgenticLoop] = {}
72
74
 
73
75
  @staticmethod
@@ -103,11 +105,15 @@ class AgentManager:
103
105
  async for event in self._run_loop(loop, request.message):
104
106
  yield event
105
107
 
106
- async def _run_loop(self, loop: AgenticLoop, message: str) -> AsyncIterator[AgentEvent]:
108
+ async def _run_loop(
109
+ self, loop: AgenticLoop, message: str
110
+ ) -> AsyncIterator[AgentEvent]:
107
111
  """Iterate one agent turn, translating callbacks to AgentEvents."""
108
112
  async for cb in loop.astream(message):
109
113
  if isinstance(cb, CompactCallback):
110
- yield CompactEvent(attempt=cb.attempt, messages_dropped=cb.messages_dropped)
114
+ yield CompactEvent(
115
+ attempt=cb.attempt, messages_dropped=cb.messages_dropped
116
+ )
111
117
  elif isinstance(cb, TokenCallback):
112
118
  yield TokenEvent(text=cb.message)
113
119
  elif isinstance(cb, MessageCallback):
@@ -121,10 +127,14 @@ class AgentManager:
121
127
  elif isinstance(cb, ToolCallback):
122
128
  if cb.tool_name == "execute_skill":
123
129
  skill_name = cb.tool_args.get("name", "")
124
- target = cb.tool_args.get("target") or cb.tool_args.get("command", "")
130
+ target = cb.tool_args.get("target") or cb.tool_args.get(
131
+ "command", ""
132
+ )
125
133
  if not self._tool_handler.is_skill_trusted(skill_name, target):
126
134
  kwargs = cb.tool_args.get("kwargs") or {}
127
- confirm = ConfirmEvent(skill_name=skill_name, target=target, kwargs=kwargs)
135
+ confirm = ConfirmEvent(
136
+ skill_name=skill_name, target=target, kwargs=kwargs
137
+ )
128
138
  yield confirm
129
139
  allowed = await confirm.wait()
130
140
  if not allowed:
@@ -137,7 +147,9 @@ class AgentManager:
137
147
  description=cb.description,
138
148
  )
139
149
  start_time = time.monotonic()
140
- result = await self._tool_handler.execute(cb.tool_name, cb.tool_args, loop._max_tool_output)
150
+ result = await self._tool_handler.execute(
151
+ cb.tool_name, cb.tool_args, loop._max_tool_output
152
+ )
141
153
  cb.set_response(result.output, is_error=result.is_error)
142
154
  duration_ms = (time.monotonic() - start_time) * 1000
143
155
  cb.duration_ms = duration_ms
@@ -150,7 +162,9 @@ class AgentManager:
150
162
 
151
163
  def _build_chain(self) -> Callable[[Request], AsyncIterator[AgentEvent]]:
152
164
  """Build the middleware chain; first middleware in the list is innermost."""
153
- current: Callable[[Request], AsyncIterator[AgentEvent]] = self._stream_events_core
165
+ current: Callable[[Request], AsyncIterator[AgentEvent]] = (
166
+ self._stream_events_core
167
+ )
154
168
  for mw in self._middlewares:
155
169
  prev = current
156
170
 
@@ -163,7 +177,9 @@ class AgentManager:
163
177
  current = make_wrapper(mw, prev)
164
178
  return current
165
179
 
166
- async def astream_events(self, session_id: str, message: str) -> AsyncIterator[AgentEvent]:
180
+ async def astream_events(
181
+ self, session_id: str, message: str
182
+ ) -> AsyncIterator[AgentEvent]:
167
183
  """Stream :class:`AgentEvent` objects for one agent turn.
168
184
 
169
185
  Tool execution and skill confirmation are handled internally.
@@ -266,14 +282,20 @@ class AgentManager:
266
282
  if active_turn_task and not active_turn_task.done():
267
283
  active_turn_task.cancel()
268
284
  else:
269
- await event_queue.put(ManagerError(message="No active turn to cancel"))
285
+ await event_queue.put(
286
+ ManagerError(message="No active turn to cancel")
287
+ )
270
288
 
271
289
  elif isinstance(cmd, (ApproveSkill, DenySkill)):
272
290
  future = pending_approvals.pop(cmd.request_id, None)
273
291
  if future and not future.done():
274
292
  future.set_result(isinstance(cmd, ApproveSkill))
275
293
  else:
276
- await event_queue.put(ManagerError(message=f"Unknown approval request: {cmd.request_id}"))
294
+ await event_queue.put(
295
+ ManagerError(
296
+ message=f"Unknown approval request: {cmd.request_id}"
297
+ )
298
+ )
277
299
 
278
300
  async def _execute_bridge_turn(
279
301
  self,
@@ -291,7 +313,9 @@ class AgentManager:
291
313
  try:
292
314
  async for event in self.astream_events(session_id, message):
293
315
  if isinstance(event, TokenEvent):
294
- await event_queue.put(TokenEmitted(turn_id=turn_id, text=event.text))
316
+ await event_queue.put(
317
+ TokenEmitted(turn_id=turn_id, text=event.text)
318
+ )
295
319
  elif isinstance(event, ToolStartEvent):
296
320
  current_tool_id = str(uuid4())
297
321
  await event_queue.put(
@@ -73,6 +73,7 @@ async def run(
73
73
  session_id,
74
74
  model=model,
75
75
  history_path=project_dir() / "history",
76
+ memory=memory,
76
77
  )
77
78
  try:
78
79
  await shell.run()
@@ -19,16 +19,6 @@ from pathlib import Path
19
19
  from typing import Any, Optional
20
20
  from uuid import uuid4
21
21
 
22
- from prompt_toolkit.application import Application
23
- from prompt_toolkit.completion import WordCompleter
24
- from prompt_toolkit.filters import Condition
25
- from prompt_toolkit.history import FileHistory
26
- from prompt_toolkit.key_binding import KeyBindings
27
- from prompt_toolkit.layout import HSplit, Layout, VSplit, Window
28
- from prompt_toolkit.layout.controls import FormattedTextControl
29
- from prompt_toolkit.styles import Style
30
- from prompt_toolkit.widgets import Frame, TextArea
31
-
32
22
  from make_agent.agent_core import (
33
23
  AgentManager,
34
24
  ApprovalRequested,
@@ -42,14 +32,24 @@ from make_agent.agent_core import (
42
32
  Shutdown,
43
33
  StartTurn,
44
34
  StatusChanged,
35
+ TokenEmitted,
45
36
  ToolFinished,
46
37
  ToolStarted,
47
- TokenEmitted,
48
38
  TurnCancelled,
49
39
  TurnFinished,
50
40
  TurnStarted,
51
41
  )
42
+ from make_agent.memory import Memory
43
+ from prompt_toolkit.application import Application
44
+ from prompt_toolkit.completion import WordCompleter
45
+ from prompt_toolkit.filters import Condition
46
+ from prompt_toolkit.key_binding import KeyBindings
47
+ from prompt_toolkit.layout import HSplit, Layout, VSplit, Window
48
+ from prompt_toolkit.layout.controls import FormattedTextControl
49
+ from prompt_toolkit.styles import Style
50
+ from prompt_toolkit.widgets import Frame, TextArea
52
51
 
52
+ from .user_messages import UserMessagesManager
53
53
 
54
54
  # ── status / enums ──────────────────────────────────────────────────────────────
55
55
 
@@ -299,6 +299,7 @@ class MakeAgentShell:
299
299
  session_id: str,
300
300
  model: str,
301
301
  history_path: Path,
302
+ memory: Optional[Memory] = None,
302
303
  ) -> None:
303
304
  self._agent_manager = agent_manager
304
305
  self._session_id = session_id
@@ -310,6 +311,7 @@ class MakeAgentShell:
310
311
  self._app: Optional[Application] = None
311
312
  self._response_area: Optional[TextArea] = None
312
313
  self._tools_area: Optional[TextArea] = None
314
+ self._history_manager = UserMessagesManager(memory) if memory else None
313
315
  self._commands: dict[str, Any] = {
314
316
  "exit": self._cmd_exit,
315
317
  "quit": self._cmd_exit,
@@ -418,7 +420,6 @@ class MakeAgentShell:
418
420
  prompt="> ",
419
421
  multiline=True,
420
422
  completer=completer,
421
- history=FileHistory(str(self._history_path)),
422
423
  wrap_lines=False,
423
424
  )
424
425
  self._composer_input = composer_input
@@ -474,7 +475,7 @@ class MakeAgentShell:
474
475
  else [
475
476
  (
476
477
  "class:hint",
477
- " /help /stats /export /exit Alt+Enter newline Ctrl+T transcript",
478
+ " /help /stats /export /exit ↑↓ history Alt+Enter newline Ctrl+T transcript",
478
479
  )
479
480
  ]
480
481
  )
@@ -557,10 +558,15 @@ class MakeAgentShell:
557
558
  def _on_enter(event) -> None:
558
559
  if state.status != AgentStatus.IDLE or state.transcript_focused:
559
560
  return
561
+
560
562
  text = composer_input.text.strip()
561
563
  if not text:
562
564
  return
565
+
563
566
  composer_input.text = ""
567
+ if self._history_manager is not None:
568
+ self._history_manager.submit() # Reset nav state after sending
569
+
564
570
  if text.startswith("/"):
565
571
  should_exit = self._dispatch_command(text[1:])
566
572
  if should_exit:
@@ -570,6 +576,31 @@ class MakeAgentShell:
570
576
  else:
571
577
  asyncio.ensure_future(self._run_turn(text))
572
578
 
579
+ # Only bind up/down for history navigation when composer is focused,
580
+ # idle, and memory is available — so arrow keys still scroll the
581
+ # transcript area when it's focused.
582
+ composer_history_filter = Condition(
583
+ lambda: (
584
+ not state.transcript_focused
585
+ and state.status == AgentStatus.IDLE
586
+ and self._history_manager is not None
587
+ )
588
+ )
589
+
590
+ @kb.add("up", filter=composer_history_filter)
591
+ def _on_up(event) -> None:
592
+ msg = self._history_manager.previous(composer_input.text)
593
+ if msg is not None:
594
+ composer_input.text = msg
595
+ self._refresh()
596
+
597
+ @kb.add("down", filter=composer_history_filter)
598
+ def _on_down(event) -> None:
599
+ msg = self._history_manager.next()
600
+ if msg is not None:
601
+ composer_input.text = msg
602
+ self._refresh()
603
+
573
604
  @kb.add("escape", "enter")
574
605
  def _on_alt_enter(event) -> None:
575
606
  if state.status == AgentStatus.IDLE and not state.transcript_focused:
@@ -0,0 +1,110 @@
1
+ """User message history manager for the agent shell.
2
+
3
+ Provides navigation through past user messages backed by persistent Memory.
4
+ The shell interacts with this manager, not Memory directly.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Optional
10
+
11
+ from make_agent.memory import Memory
12
+
13
+
14
+ class UserMessagesManager:
15
+ """Manages user message history for shell navigation.
16
+
17
+ Refreshes messages from Memory on each navigation start so that
18
+ messages from the current session are always included. Supports
19
+ forward/backward navigation with index tracking.
20
+ """
21
+
22
+ def __init__(self, memory: Memory, limit: int = 200) -> None:
23
+ """Initialize with a Memory instance.
24
+
25
+ Args:
26
+ memory: The Memory backend to fetch messages from.
27
+ limit: Maximum number of historical messages to load (default: 200).
28
+ """
29
+ self._memory = memory
30
+ self._limit = limit
31
+ self._messages: list[str] = [] # Refreshed on each nav start, newest first
32
+ self._index: int = -1 # -1 = not navigating; 0 = most recent
33
+ self._original_text: str = "" # Text user had typed before pressing UP
34
+
35
+ def _refresh(self) -> None:
36
+ """Reload user messages from Memory so current-session messages
37
+ are always visible."""
38
+ self._messages = self._memory.recent_user(self._limit)
39
+
40
+ def start_navigating(self, current_text: str) -> Optional[str]:
41
+ """Begin navigation from the current composer text.
42
+
43
+ Called when user first presses UP. Reloads messages from Memory
44
+ so current-session messages are included. Saves the current text
45
+ as the restoration point (returned when DOWN exits navigation).
46
+
47
+ Args:
48
+ current_text: What the user has typed so far.
49
+
50
+ Returns:
51
+ The most recent past user message, or None if history is empty.
52
+ """
53
+ self._refresh()
54
+ if not self._messages:
55
+ return None
56
+ self._original_text = current_text
57
+ self._index = 0
58
+ return self._messages[0]
59
+
60
+ def previous(self, current_text: str = "") -> Optional[str]:
61
+ """Navigate to the next older message.
62
+
63
+ Args:
64
+ current_text: What the user has typed so far (saved as restoration point
65
+ on first UP press).
66
+
67
+ Returns:
68
+ The previous message, or None if already at the oldest.
69
+ """
70
+ if self._index < 0:
71
+ # First press — start navigating, save current text as restoration point
72
+ return self.start_navigating(current_text)
73
+ if self._index < len(self._messages) - 1:
74
+ self._index += 1
75
+ return self._messages[self._index]
76
+ return None
77
+
78
+ def next(self) -> Optional[str]:
79
+ """Navigate to the next newer message or restore original text.
80
+
81
+ Returns:
82
+ The next newer message, or the original typed text when
83
+ navigation returns to the start, or None if not navigating.
84
+ """
85
+ if self._index < 0:
86
+ return None
87
+ if self._index > 0:
88
+ self._index -= 1
89
+ return self._messages[self._index]
90
+ # Back at start — restore original text
91
+ text = self._original_text
92
+ self._index = -1
93
+ self._original_text = ""
94
+ return text
95
+
96
+ def cancel(self) -> None:
97
+ """Cancel navigation and restore the original typed text."""
98
+ self._index = -1
99
+
100
+ def submit(self) -> None:
101
+ """Called when the user sends a message (presses Enter).
102
+
103
+ Resets navigation state so the next UP press starts fresh.
104
+ """
105
+ self._index = -1
106
+ self._original_text = ""
107
+
108
+ def is_navigating(self) -> bool:
109
+ """Check if the user is currently in navigation mode."""
110
+ return self._index >= 0
@@ -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
@@ -37,7 +38,9 @@ def _init_logging(loglevel: str) -> None:
37
38
  # Remove any existing root handlers so basicConfig always adds our file handler.
38
39
  for h in logging.root.handlers[:]:
39
40
  logging.root.removeHandler(h)
40
- logging.basicConfig(filename=log_file(), level=level, format="%(asctime)s %(levelname)s %(message)s")
41
+ logging.basicConfig(
42
+ filename=log_file(), level=level, format="%(asctime)s %(levelname)s %(message)s"
43
+ )
41
44
 
42
45
 
43
46
  def _resolve_system_prompt(args: argparse.Namespace) -> str:
@@ -76,7 +79,10 @@ def _parse_disabled_tools(value: str | None, mode: str) -> frozenset[str]:
76
79
  names = frozenset(name.strip() for name in value.split(",") if name.strip())
77
80
  unknown = names - available
78
81
  if unknown:
79
- sys.exit("make-agent: unknown built-in tool(s): " f"{', '.join(sorted(unknown))}. Valid names for {mode}: {', '.join(sorted(available))}")
82
+ sys.exit(
83
+ "make-agent: unknown built-in tool(s): "
84
+ f"{', '.join(sorted(unknown))}. Valid names for {mode}: {', '.join(sorted(available))}"
85
+ )
80
86
  return names
81
87
 
82
88
 
@@ -89,8 +95,36 @@ def _parse_trusted_skills(value: str | None) -> frozenset[str]:
89
95
  return frozenset(name.strip() for name in value.split(",") if name.strip())
90
96
 
91
97
 
92
- def _build_backend(skill_mode: str, skills_dir: str, tool_timeout: int):
93
- 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
+ )
94
128
 
95
129
 
96
130
  def _cmd_run(args: argparse.Namespace) -> None:
@@ -112,8 +146,13 @@ def _cmd_run(args: argparse.Namespace) -> None:
112
146
  else:
113
147
  skills_dir = str(default_skills_dir(_SKILL_MODE))
114
148
 
149
+ all_names = _discover_skill_names(skills_dir)
150
+ enabled_skills = _parse_enabled_skills(args.enabled_skills, frozenset(all_names))
151
+
115
152
  memory = Memory(mode_memory_path(_SKILL_MODE))
116
- backend = _build_backend(_SKILL_MODE, skills_dir, args.tool_timeout)
153
+ backend = MakefileSkillBackend(
154
+ skills_dir, DEFAULT_TOOL_TIMEOUT, Path.cwd(), enabled_skills
155
+ )
117
156
  trusted_skills = _parse_trusted_skills(getattr(args, "trusted_skills", None))
118
157
  tool_handler = ToolHandler(backend, memory, disabled, trusted_skills)
119
158
 
@@ -143,7 +182,9 @@ def main() -> None:
143
182
  subparsers = parser.add_subparsers(dest="command")
144
183
 
145
184
  run_p = subparsers.add_parser("run", help="Start the interactive agent (default)")
146
- run_p.add_argument("--model", default=None, metavar="MODEL", help="litellm model string (required)")
185
+ run_p.add_argument(
186
+ "--model", default=None, metavar="MODEL", help="litellm model string (required)"
187
+ )
147
188
  system_g = run_p.add_mutually_exclusive_group()
148
189
  system_g.add_argument(
149
190
  "--system",
@@ -224,6 +265,12 @@ def main() -> None:
224
265
  metavar="EFFORT",
225
266
  help=f"Reasoning effort level ({'/'.join(_REASONING_EFFORT_VALUES)}, default: auto)",
226
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
+ )
227
274
  run_p.add_argument(
228
275
  "--trusted-skills",
229
276
  default=None,
@@ -216,6 +216,21 @@ class Memory:
216
216
  """Search past agent replies using FTS5 via the ``agent_memory`` view."""
217
217
  return self._search("agent_memory", query, limit, from_date, to_date)
218
218
 
219
+ def recent_user(
220
+ self,
221
+ limit: int = 10,
222
+ ) -> list[str]:
223
+ """Return the *limit* most recent user messages as plain strings, newest first.
224
+
225
+ Used by the shell for UP/DOWN arrow navigation through past user messages.
226
+ """
227
+ conn = self._get_conn()
228
+ rows = conn.execute(
229
+ "SELECT message FROM messages WHERE sender = 'user' ORDER BY id DESC LIMIT ?",
230
+ (limit,),
231
+ ).fetchall()
232
+ return [row["message"] for row in rows]
233
+
219
234
  def recent(
220
235
  self,
221
236
  limit: int = 10,
@@ -11,7 +11,9 @@ from make_agent.builtin_tools.skill_tools import create_skill as create_makefile
11
11
  from make_agent.builtin_tools.skill_tools import execute_skill as execute_makefile_skill
12
12
  from make_agent.builtin_tools.skill_tools import list_skills as list_makefile_skills
13
13
  from make_agent.builtin_tools.skill_tools import read_skill as read_makefile_skill
14
- from make_agent.builtin_tools.skill_tools import validate_skill as validate_makefile_skill
14
+ from make_agent.builtin_tools.skill_tools import (
15
+ validate_skill as validate_makefile_skill,
16
+ )
15
17
 
16
18
 
17
19
  class SkillBackend(Protocol):
@@ -30,14 +32,20 @@ class MakefileSkillBackend:
30
32
  skills_dir: str,
31
33
  tool_timeout: int = 600,
32
34
  base_dir: Path | None = None,
35
+ enabled_skills: frozenset[str] | None = None,
33
36
  ) -> None:
34
37
  self._skills_dir = skills_dir
35
38
  self._tool_timeout = tool_timeout
36
39
  self._base_dir = base_dir if base_dir is not None else Path.cwd()
40
+ self._enabled_skills = enabled_skills
37
41
  self._schemas = MAKEFILE_SKILL_SCHEMAS + FILE_SCHEMAS
38
42
  self._executors: dict[str, Any] = {
39
- "list_skills": lambda **_kw: list_makefile_skills(self._skills_dir),
40
- "read_skill": lambda name, **_kw: read_makefile_skill(name, self._skills_dir),
43
+ "list_skills": lambda **_kw: list_makefile_skills(
44
+ self._skills_dir, self._enabled_skills
45
+ ),
46
+ "read_skill": lambda name, **_kw: read_makefile_skill(
47
+ name, self._skills_dir
48
+ ),
41
49
  "execute_skill": lambda name, command, **_kw: execute_makefile_skill(
42
50
  name,
43
51
  command,
@@ -26,12 +26,28 @@ class ToolHandler:
26
26
  disabled: frozenset[str] = frozenset(),
27
27
  trusted_skills: frozenset[str] = frozenset(),
28
28
  ) -> None:
29
- active_backend_schemas = [schema for schema in backend.schemas if schema["function"]["name"] not in disabled]
30
- active_memory_schemas = [schema for schema in MEMORY_SCHEMAS if schema["function"]["name"] not in disabled]
29
+ active_backend_schemas = [
30
+ schema
31
+ for schema in backend.schemas
32
+ if schema["function"]["name"] not in disabled
33
+ ]
34
+ active_memory_schemas = [
35
+ schema
36
+ for schema in MEMORY_SCHEMAS
37
+ if schema["function"]["name"] not in disabled
38
+ ]
31
39
  self._schemas: list[dict] = active_backend_schemas + active_memory_schemas
32
40
  self._executors: dict[str, Any] = {
33
- **{name: executor for name, executor in backend.executors.items() if name not in disabled},
34
- **{name: executor for name, executor in get_memory_executors(memory).items() if name not in disabled},
41
+ **{
42
+ name: executor
43
+ for name, executor in backend.executors.items()
44
+ if name not in disabled
45
+ },
46
+ **{
47
+ name: executor
48
+ for name, executor in get_memory_executors(memory).items()
49
+ if name not in disabled
50
+ },
35
51
  }
36
52
  self._backend = backend
37
53
  self._trusted_skills = trusted_skills
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: makefile-agent
3
- Version: 0.4.1
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
@@ -20,6 +20,7 @@ make_agent/agent_core/provider.py
20
20
  make_agent/agent_shell/__init__.py
21
21
  make_agent/agent_shell/run.py
22
22
  make_agent/agent_shell/shell.py
23
+ make_agent/agent_shell/user_messages.py
23
24
  make_agent/builtin_tools/__init__.py
24
25
  make_agent/builtin_tools/file_tools.py
25
26
  make_agent/builtin_tools/skill_tools.py
@@ -47,9 +48,11 @@ tests/test_bridge.py
47
48
  tests/test_builtin_tools.py
48
49
  tests/test_commands.py
49
50
  tests/test_compact.py
51
+ tests/test_enabled_skills.py
50
52
  tests/test_main.py
51
53
  tests/test_memory.py
52
54
  tests/test_middleware.py
53
55
  tests/test_parser.py
54
56
  tests/test_tools.py
55
- tests/test_trusted_skill.py
57
+ tests/test_trusted_skill.py
58
+ tests/test_user_messages.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "makefile-agent"
3
- version = "0.4.1"
3
+ version = "0.4.3"
4
4
  description = "An AI agent powered by Makefile skills."
5
5
  readme = "README.md"
6
6
  license = "MIT"