autoforge-ai 0.1.7 → 0.1.8

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.
@@ -984,5 +984,35 @@ def feature_set_dependencies(
984
984
  return json.dumps({"error": f"Failed to set dependencies: {str(e)}"})
985
985
 
986
986
 
987
+ @mcp.tool()
988
+ def ask_user(
989
+ questions: Annotated[list[dict], Field(description="List of questions to ask, each with question, header, options (list of {label, description}), and multiSelect (bool)")]
990
+ ) -> str:
991
+ """Ask the user structured questions with selectable options.
992
+
993
+ Use this when you need clarification or want to offer choices to the user.
994
+ Each question has a short header, the question text, and 2-4 clickable options.
995
+ The user's selections will be returned as your next message.
996
+
997
+ Args:
998
+ questions: List of questions, each with:
999
+ - question (str): The question to ask
1000
+ - header (str): Short label (max 12 chars)
1001
+ - options (list): Each with label (str) and description (str)
1002
+ - multiSelect (bool): Allow multiple selections (default false)
1003
+
1004
+ Returns:
1005
+ Acknowledgment that questions were presented to the user
1006
+ """
1007
+ # Validate input
1008
+ for i, q in enumerate(questions):
1009
+ if not all(key in q for key in ["question", "header", "options"]):
1010
+ return json.dumps({"error": f"Question at index {i} missing required fields"})
1011
+ if len(q["options"]) < 2 or len(q["options"]) > 4:
1012
+ return json.dumps({"error": f"Question at index {i} must have 2-4 options"})
1013
+
1014
+ return "Questions presented to the user. Their response will arrive as your next message."
1015
+
1016
+
987
1017
  if __name__ == "__main__":
988
1018
  mcp.run()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autoforge-ai",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Autonomous coding agent with web UI - build complete apps with AI",
5
5
  "license": "AGPL-3.0",
6
6
  "bin": {
@@ -207,12 +207,14 @@ async def assistant_chat_websocket(websocket: WebSocket, project_name: str):
207
207
  Client -> Server:
208
208
  - {"type": "start", "conversation_id": int | null} - Start/resume session
209
209
  - {"type": "message", "content": "..."} - Send user message
210
+ - {"type": "answer", "answers": {...}} - Answer to structured questions
210
211
  - {"type": "ping"} - Keep-alive ping
211
212
 
212
213
  Server -> Client:
213
214
  - {"type": "conversation_created", "conversation_id": int} - New conversation created
214
215
  - {"type": "text", "content": "..."} - Text chunk from Claude
215
216
  - {"type": "tool_call", "tool": "...", "input": {...}} - Tool being called
217
+ - {"type": "question", "questions": [...]} - Structured questions for user
216
218
  - {"type": "response_done"} - Response complete
217
219
  - {"type": "error", "content": "..."} - Error message
218
220
  - {"type": "pong"} - Keep-alive pong
@@ -303,6 +305,34 @@ async def assistant_chat_websocket(websocket: WebSocket, project_name: str):
303
305
  async for chunk in session.send_message(user_content):
304
306
  await websocket.send_json(chunk)
305
307
 
308
+ elif msg_type == "answer":
309
+ # User answered a structured question
310
+ if not session:
311
+ session = get_session(project_name)
312
+ if not session:
313
+ await websocket.send_json({
314
+ "type": "error",
315
+ "content": "No active session. Send 'start' first."
316
+ })
317
+ continue
318
+
319
+ # Format the answers as a natural response
320
+ answers = message.get("answers", {})
321
+ if isinstance(answers, dict):
322
+ response_parts = []
323
+ for question_idx, answer_value in answers.items():
324
+ if isinstance(answer_value, list):
325
+ response_parts.append(", ".join(answer_value))
326
+ else:
327
+ response_parts.append(str(answer_value))
328
+ user_response = "; ".join(response_parts) if response_parts else "OK"
329
+ else:
330
+ user_response = str(answers)
331
+
332
+ # Stream Claude's response
333
+ async for chunk in session.send_message(user_response):
334
+ await websocket.send_json(chunk)
335
+
306
336
  else:
307
337
  await websocket.send_json({
308
338
  "type": "error",
@@ -47,8 +47,13 @@ FEATURE_MANAGEMENT_TOOLS = [
47
47
  "mcp__features__feature_skip",
48
48
  ]
49
49
 
50
+ # Interactive tools
51
+ INTERACTIVE_TOOLS = [
52
+ "mcp__features__ask_user",
53
+ ]
54
+
50
55
  # Combined list for assistant
51
- ASSISTANT_FEATURE_TOOLS = READONLY_FEATURE_MCP_TOOLS + FEATURE_MANAGEMENT_TOOLS
56
+ ASSISTANT_FEATURE_TOOLS = READONLY_FEATURE_MCP_TOOLS + FEATURE_MANAGEMENT_TOOLS + INTERACTIVE_TOOLS
52
57
 
53
58
  # Read-only built-in tools (no Write, Edit, Bash)
54
59
  READONLY_BUILTIN_TOOLS = [
@@ -123,6 +128,9 @@ If the user asks you to modify code, explain that you're a project assistant and
123
128
  - **feature_create_bulk**: Create multiple features at once
124
129
  - **feature_skip**: Move a feature to the end of the queue
125
130
 
131
+ **Interactive:**
132
+ - **ask_user**: Present structured multiple-choice questions to the user. Use this when you need to clarify requirements, offer design choices, or guide a decision. The user sees clickable option buttons and their selection is returned as your next message.
133
+
126
134
  ## Creating Features
127
135
 
128
136
  When a user asks to add a feature, use the `feature_create` or `feature_create_bulk` MCP tools directly:
@@ -402,6 +410,17 @@ class AssistantChatSession:
402
410
  elif block_type == "ToolUseBlock" and hasattr(block, "name"):
403
411
  tool_name = block.name
404
412
  tool_input = getattr(block, "input", {})
413
+
414
+ # Intercept ask_user tool calls -> yield as question message
415
+ if tool_name == "mcp__features__ask_user":
416
+ questions = tool_input.get("questions", [])
417
+ if questions:
418
+ yield {
419
+ "type": "question",
420
+ "questions": questions,
421
+ }
422
+ continue
423
+
405
424
  yield {
406
425
  "type": "tool_call",
407
426
  "tool": tool_name,