From 44a98c7d02d77855990a45ef6143634d1aab271e Mon Sep 17 00:00:00 2001 From: FunctionFreak Date: Thu, 18 Jun 2026 15:40:09 +0100 Subject: [PATCH 1/5] shell execution return command fix --- Auto_Use/macOS_use/controller/cli/service.py | 28 +++++++++++++------ Auto_Use/macOS_use/controller/tool/shell.py | 27 +++++++++++++----- .../windows_use/controller/cli/service.py | 25 ++++++++++++----- Auto_Use/windows_use/controller/tool/shell.py | 24 ++++++++++++---- main.py | 2 +- 5 files changed, 77 insertions(+), 29 deletions(-) diff --git a/Auto_Use/macOS_use/controller/cli/service.py b/Auto_Use/macOS_use/controller/cli/service.py index 3d4517e..71b3796 100644 --- a/Auto_Use/macOS_use/controller/cli/service.py +++ b/Auto_Use/macOS_use/controller/cli/service.py @@ -175,23 +175,35 @@ def shell(self, command: str, input_text: str = None) -> dict: output += result["stdout"] if result.get("stderr"): output += result["stderr"] - + + returncode = result.get("returncode") + succeeded = result.get("success") + response = { - "status": "success" if result.get("success") else "error", + "status": "success" if succeeded else "error", "action": "shell", "cwd": self.sandbox.get_cwd(), "command": command } - - # Only include output if there's actual content - if output.strip(): - response["output"] = output.strip() - + + # Always surface the exit code so the agent can tell a real error + # from an empty result (e.g. grep exits 1 on no match). + if returncode is not None: + response["exit_code"] = returncode + + # Show exactly what the command returned (verbatim stdout + stderr). + # When the terminal returns absolutely nothing, show a clear marker + # instead of a blank string — the status/exit_code already say whether + # it succeeded, so this just makes "no output" unambiguous (like a + # real terminal / Claude Code). + clean_output = output.strip() + response["output"] = clean_output if clean_output else "(no output)" + # Only include error if there's actual content error = result.get("error", "") if error: response["error"] = error - + return response except Exception as e: diff --git a/Auto_Use/macOS_use/controller/tool/shell.py b/Auto_Use/macOS_use/controller/tool/shell.py index 0d1ad64..07a2d62 100644 --- a/Auto_Use/macOS_use/controller/tool/shell.py +++ b/Auto_Use/macOS_use/controller/tool/shell.py @@ -92,21 +92,34 @@ def run(self, command: str, input_text: str = None) -> dict: output += result["stdout"] if result.get("stderr"): output += result["stderr"] - + + returncode = result.get("returncode") + succeeded = result.get("success") + response = { - "status": "success" if result.get("success") else "failed", + "status": "success" if succeeded else "failed", "action": "shell", "agent_location": agent_location, "shell": command, } - - if output.strip(): - response["output"] = output.strip() - + + # Always surface the exit code so the agent can tell a real error + # from an empty result (e.g. grep exits 1 on no match). + if returncode is not None: + response["exit_code"] = returncode + + # Show exactly what the command returned (verbatim stdout + stderr). + # When the terminal returns absolutely nothing, show a clear marker + # instead of a blank string — the status/exit_code already say whether + # it succeeded, so this just makes "no output" unambiguous (like a + # real terminal / Claude Code). + clean_output = output.strip() + response["output"] = clean_output if clean_output else "(no output)" + error = result.get("error", "") if error: response["error"] = error - + return response except Exception as e: diff --git a/Auto_Use/windows_use/controller/cli/service.py b/Auto_Use/windows_use/controller/cli/service.py index 71b3ddd..33f01b2 100644 --- a/Auto_Use/windows_use/controller/cli/service.py +++ b/Auto_Use/windows_use/controller/cli/service.py @@ -178,23 +178,34 @@ def shell(self, command: str, input_text: str = None) -> dict: output += result["stdout"] if result.get("stderr"): output += result["stderr"] - + + returncode = result.get("returncode") + response = { "status": "success" if result.get("success") else "error", "action": "shell", "cwd": self.sandbox.get_cwd(), "command": command } - - # Only include output if there's actual content - if output.strip(): - response["output"] = output.strip() - + + # Always surface the exit code so the agent can tell a real error + # from an empty result. + if returncode is not None: + response["exit_code"] = returncode + + # Show exactly what the command returned (verbatim stdout + stderr). + # When the terminal returns absolutely nothing, show a clear marker + # instead of a blank string — the status/exit_code already say whether + # it succeeded, so this just makes "no output" unambiguous (like a + # real terminal / Claude Code). + clean_output = output.strip() + response["output"] = clean_output if clean_output else "(no output)" + # Only include error if there's actual content error = result.get("error", "") if error: response["error"] = error - + return response except Exception as e: diff --git a/Auto_Use/windows_use/controller/tool/shell.py b/Auto_Use/windows_use/controller/tool/shell.py index 81c85b1..7e15e1d 100644 --- a/Auto_Use/windows_use/controller/tool/shell.py +++ b/Auto_Use/windows_use/controller/tool/shell.py @@ -94,21 +94,33 @@ def run(self, command: str, input_text: str = None) -> dict: output += result["stdout"] if result.get("stderr"): output += result["stderr"] - + + returncode = result.get("returncode") + response = { "status": "success" if result.get("success") else "failed", "action": "shell", "agent_location": agent_location, "shell": command, } - - if output.strip(): - response["output"] = output.strip() - + + # Always surface the exit code so the agent can tell a real error + # from an empty result. + if returncode is not None: + response["exit_code"] = returncode + + # Show exactly what the command returned (verbatim stdout + stderr). + # When the terminal returns absolutely nothing, show a clear marker + # instead of a blank string — the status/exit_code already say whether + # it succeeded, so this just makes "no output" unambiguous (like a + # real terminal / Claude Code). + clean_output = output.strip() + response["output"] = clean_output if clean_output else "(no output)" + error = result.get("error", "") if error: response["error"] = error - + return response except Exception as e: diff --git a/main.py b/main.py index 3035dcc..c1565db 100644 --- a/main.py +++ b/main.py @@ -29,7 +29,7 @@ # Configuration PROVIDER = "openrouter" -MODEL = "gemini-3.1-pro" #refer to the model name correctly from model_list.txt. +MODEL = "gemini-3.5-flash" #refer to the model name correctly from model_list.txt. # Your task here task = """ From 925c4077486b22e9715f2341975fcf04c073f6f5 Mon Sep 17 00:00:00 2001 From: FunctionFreak Date: Thu, 18 Jun 2026 15:43:40 +0100 Subject: [PATCH 2/5] scratchpad repetitive information --- Auto_Use/macOS_use/agent/main_driver/system_prompt.md | 5 +++-- Auto_Use/windows_use/agent/main_driver/system_prompt.md | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Auto_Use/macOS_use/agent/main_driver/system_prompt.md b/Auto_Use/macOS_use/agent/main_driver/system_prompt.md index efe6367..ea02e8b 100644 --- a/Auto_Use/macOS_use/agent/main_driver/system_prompt.md +++ b/Auto_Use/macOS_use/agent/main_driver/system_prompt.md @@ -145,8 +145,9 @@ Each step includes: 2. Only write after visual confirmation — never assume success. 3. Write immediately when something is confirmed. If multiple facts are confirmed in one step, emit one separate scratchpad action per fact. 4. Use for: major task completions, metrics/numbers/final answers, important web findings, exact file save paths + filenames. -5. Format: {"type": "scratchpad", "value": "one-line_verified_note"} -6. Examples: +5.Avoid writing repetitive information. +6. Format: {"type": "scratchpad", "value": "one-line_verified_note"} +7. Examples: 1. {"type": "scratchpad", "value": "Done: Email sent to abc@gmail.com with flight details + attachments"} 2. {"type": "scratchpad", "value": "Saved abc.pdf to ~/Documents/testing/abc.pdf"} 3. {"type": "scratchpad", "value": "Key metric: Disney+ revenue (Q3 2025) = 2.1B $"} diff --git a/Auto_Use/windows_use/agent/main_driver/system_prompt.md b/Auto_Use/windows_use/agent/main_driver/system_prompt.md index d52fb00..ba382bd 100644 --- a/Auto_Use/windows_use/agent/main_driver/system_prompt.md +++ b/Auto_Use/windows_use/agent/main_driver/system_prompt.md @@ -131,7 +131,8 @@ Each step includes: 2. Only write after visual confirmation — never assume success. 3. Write immediately when something is confirmed. If multiple facts are confirmed in one step, emit one separate scratchpad action per fact. 4. Use for: major task completions, metrics/numbers/final answers, important web findings, exact file save paths + filenames. -5. Examples: +5. Avoid writing repetitive information. +6. Examples: 1. {"type": "scratchpad", "value": "Done: Email sent to abc@gmail.com with flight details + attachments"} 2. {"type": "scratchpad", "value": "Saved abc.pdf to D:\\Drive\\testing\\abc.pdf"} 3. {"type": "scratchpad", "value": "Key metric: Disney+ revenue (Q3 2025) = 2.1B $"} From 2591f4c3050c7aff22dacf0d1181563c1de68219 Mon Sep 17 00:00:00 2001 From: FunctionFreak Date: Thu, 18 Jun 2026 17:55:44 +0100 Subject: [PATCH 3/5] fixed todo and scratchpad location in main driver agent --- .../macOS_use/agent/main_driver/service.py | 219 ++-- .../agent/main_driver/system_prompt.md | 6 +- .../windows_use/agent/main_driver/service.py | 219 ++-- .../agent/main_driver/system_prompt.md | 6 +- main.py | 3 +- windows_binary_build (1).py | 1016 +++++++++++++++++ 6 files changed, 1312 insertions(+), 157 deletions(-) create mode 100644 windows_binary_build (1).py diff --git a/Auto_Use/macOS_use/agent/main_driver/service.py b/Auto_Use/macOS_use/agent/main_driver/service.py index 34a6d85..a575737 100644 --- a/Auto_Use/macOS_use/agent/main_driver/service.py +++ b/Auto_Use/macOS_use/agent/main_driver/service.py @@ -56,7 +56,7 @@ def _compress_screenshot(base64_str: str, max_width: int = 1080, quality: int = def _cleanup_scratchpad(): """Clear all contents inside Auto_Use/macOS_use/scratchpad/ for a fresh start.""" # Clear scratchpad contents - scratchpad_dir = Path(__file__).parent.parent / "scratchpad" + scratchpad_dir = Path(__file__).parent.parent.parent / "scratchpad" if scratchpad_dir.exists(): for item in scratchpad_dir.iterdir(): if item.is_dir(): @@ -179,45 +179,48 @@ def _initialize_conversation_file(self): except Exception as e: print(f"Error initializing conversation file: {str(e)}") - def _save_conversation_snapshot(self, assistant_messages: list, user_message: str, current_assistant_response: str, image_sent: bool, interaction_count: int): - """Save a new numbered conversation file for this interaction - TRUE agent memory""" + def _save_conversation_snapshot(self, messages: list, current_assistant_response: str, image_sent: bool, interaction_count: int): + """Save a numbered conversation file rendering the EXACT payload sent this + step (system + interleaved assistant/user turns + current user message) plus + this step's freshly generated response — a faithful peek into agent memory.""" try: - # Create numbered filename: conversation_1.txt, conversation_2.txt, etc. conversation_file = self.conversation_dir / f"conversation_{interaction_count}.txt" - + + def _text(content): + # Cached messages carry content as a list of {type,text,...} blocks. + if isinstance(content, list): + return "\n".join(b.get("text", "") for b in content if isinstance(b, dict)) + return content if isinstance(content, str) else str(content) + with open(conversation_file, 'w', encoding='utf-8') as f: - # Write header + # Header f.write("=== CONVERSATION LOG ===\n") f.write(f"Session Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"Provider: {self.llm_manager.get_provider_name()}\n") f.write(f"Model: {self.llm_manager.get_model_name()}\n") f.write(f"Current Interaction: #{interaction_count}\n") f.write("=" * 60 + "\n\n") - - # Write system prompt - f.write("=== SYSTEM PROMPT ===\n") - f.write(self.system_prompt) - f.write("\n\n" + "=" * 60 + "\n\n") - - # Write all previous assistant messages (agent history) with step markers - for i, assistant_msg in enumerate(assistant_messages, 1): - f.write(f"INTERACTION #{i}\n") - f.write("=" * 60 + "\n") - f.write(f"ASSISTANT:\n\n{assistant_msg}\n\n") - f.write("\n") - - # Write current interaction (user message + current assistant response) - f.write(f"INTERACTION #{interaction_count}\n") - f.write("=" * 60 + "\n") - f.write(f"USER:\n{user_message}\n") + + # Render every message in the exact order it was sent to the model. + for m in messages: + role = str(m.get("role", "?")).upper() + if role == "SYSTEM": + f.write("=== SYSTEM PROMPT ===\n") + f.write(_text(m.get("content", ""))) + f.write("\n\n" + "=" * 60 + "\n\n") + else: + f.write(f"--- {role} ---\n") + f.write(_text(m.get("content", ""))) + f.write("\n\n") + if image_sent: - f.write("\n[Screenshot sent]\n") - f.write("\n") - - # Write current assistant response with step marker - f.write(f"ASSISTANT:\n\n{current_assistant_response}\n\n") + f.write("[Screenshot sent with the latest user message]\n\n") + + # This step's freshly generated response (the reply, not part of the request). + f.write(f"--- ASSISTANT (response · step {interaction_count}) ---\n") + f.write(current_assistant_response) f.write("\n") - + print(f"✓ Memory snapshot saved: conversation_{interaction_count}.txt") except Exception as e: print(f"Error saving conversation snapshot: {str(e)}") @@ -233,15 +236,17 @@ def _save_raw_response(self, raw_response: str, step_number: int): except Exception as e: print(f"⚠ Error saving raw response: {str(e)}") - def _save_conversation(self, assistant_messages: list, user_message: str, current_assistant_response: str, image_sent: bool, interaction_count: int): + def _save_conversation(self, messages: list, current_assistant_response: str, image_sent: bool, interaction_count: int): """Save conversation snapshot to file - simple and direct""" if self.save_conversation: - self._save_conversation_snapshot(assistant_messages, user_message, current_assistant_response, image_sent, interaction_count) + self._save_conversation_snapshot(messages, current_assistant_response, image_sent, interaction_count) def _read_todo_from_file(self) -> str: """Read the current todo list from scratchpad/todo/todo.md file""" try: - todo_file = Path(__file__).parent.parent / "scratchpad" / "todo" / "todo.md" + # Read from the task tracker's own file path (single source of truth — + # exactly where TaskTrackerService writes), so read can never drift from write. + todo_file = Path(self.controller.task_tracker.todo_file) if todo_file.exists(): with open(todo_file, 'r', encoding='utf-8') as f: return f.read().strip() @@ -254,7 +259,8 @@ def _read_todo_from_file(self) -> str: def _read_scratchpad_from_file(self) -> str: """Read the current scratchpad entries from scratchpad/milestone/milestone.md file""" try: - scratchpad_file = Path(__file__).parent.parent / "scratchpad" / "milestone" / "milestone.md" + # Read from the scratchpad service's own file path (single source of truth). + scratchpad_file = Path(self.controller.scratchpad_service.scratchpad_file) if scratchpad_file.exists(): with open(scratchpad_file, 'r', encoding='utf-8') as f: return f.read().strip() @@ -301,7 +307,52 @@ def _remove_thinking_from_response(self, response_json: str) -> str: return json.dumps(response_data, indent=2, ensure_ascii=False) except Exception: return response_json - + + def _tool_response_for_memory(self, action_result: dict) -> dict: + """Build the compact tool_response preserved in agent memory for a step. + + Only web-tool results are compacted: their raw text is already removed and + digested into the scratchpad, so memory keeps just the query + a pointer. + Web is compacted ONLY when it succeeded — a failed web call keeps its exact + response so the agent can see what went wrong. Every other result (shell + output, click/input results, etc.) is preserved verbatim. + """ + import copy + res = copy.deepcopy(action_result) + + def compact(entry): + if (isinstance(entry, dict) + and entry.get("tool") == "web" + and entry.get("status") == "success"): + return { + "action": "tool", + "tool": "web", + "query": entry.get("query", ""), + "message": "memory optimized — refer to scratchpad for the web result", + } + return entry + + if res.get("action") == "multiple" and "results" in res: + res["results"] = [compact(r) for r in res["results"]] + return res + return compact(res) + + def _trim_history_entry(self, entry: str) -> str: + """Trim an OLDER history step for context: drop 'thinking', 'eval', and + 'action' (keep decision/memory/next_goal). Only the most recent step is kept + full so the agent retains its latest reasoning; everything older is trimmed. + Handles the leading '' marker. + """ + m = re.match(r'(\n)(.*)', entry, re.DOTALL) + prefix, json_part = (m.group(1), m.group(2)) if m else ("", entry) + try: + data = json.loads(json_part) + for field in ("thinking", "eval", "action"): + data.pop(field, None) + return prefix + json.dumps(data, indent=2, ensure_ascii=False) + except Exception: + return entry + def process_request(self, task: str) -> dict: """Process a user request in an iterative loop until completion. @@ -317,6 +368,7 @@ def process_request(self, task: str) -> dict: last_response = None is_first_iteration = True assistant_messages = [] # Track all assistant responses for memory + tool_responses = [] # Per-step tool result, aligned 1:1 with assistant_messages; replayed as user turns cli_await_result = None # Stores cli_await result for next iteration's light message pending_web_response = None # Stores web tool response for light digest iteration json_fail_count = 0 # Track consecutive JSON parse failures (max 3 before exit) @@ -591,43 +643,45 @@ def process_request(self, task: str) -> dict: user_message += "\n\nAnnotated screenshot with bounding boxes" user_message += "\n\nPure vision first: decide which element to interact with from the screenshot alone, then refer to for its [id]." - # Create modified assistant messages for both API and saving - # Also strips 'action' from all middle entries (keeps only last entry's action) - messages_for_api = [] - if len(assistant_messages) > 0 and not is_first_iteration: - # Step 1: prepend user task + strip action (reinforces objective in context) - step1_msg = self._remove_action_from_response(assistant_messages[0]) - messages_for_api.append(f"\n{task}\n\n\n{step1_msg}") - - # Remaining entries except last: strip action to save tokens - for msg in assistant_messages[1:-1]: - messages_for_api.append(self._remove_action_from_response(msg)) - - # Last entry: keep action intact (needed for eval analysis) - if len(assistant_messages) > 1: - messages_for_api.append(assistant_messages[-1]) - else: - # Use original messages - messages_for_api = assistant_messages.copy() - - # Prepare messages for API - include all past assistant messages + # Build the API messages as a real interleaved dialogue: + # system, then for each past step: assistant(step) -> user(tool_response), + # and finally the current user message (live screen + ). + # + # Sliding-window trim: only the MOST RECENT past step keeps its full + # thinking/eval/action; every older step is trimmed to + # decision/memory/next_goal (_trim_history_entry), so the agent always sees + # its latest reasoning in full and just the outline of older steps. + # Tool results stay on the USER side (correct role -> clean eval, no schema + # leakage). The most recent step's result is NOT re-emitted here — it already + # rides in the current user message as . messages = [ {"role": "system", "content": self.system_prompt} ] - - # Add all assistant messages (with Step 1 replaced if applicable) - for assistant_msg in messages_for_api: - messages.append({"role": "assistant", "content": assistant_msg}) - # Add current user message + n_hist = len(assistant_messages) + for i, step_msg in enumerate(assistant_messages): + is_recent = (i == n_hist - 1) + content = step_msg if is_recent else self._trim_history_entry(step_msg) + # Reinforce the objective by prepending the task to step 1. + if i == 0 and not is_first_iteration: + content = f"\n{task}\n\n\n{content}" + messages.append({"role": "assistant", "content": content}) + + # Interleave this step's tool result as its own user turn — except the + # most recent, whose result is carried by the current user message below. + if not is_recent: + tr = tool_responses[i] if i < len(tool_responses) else None + if tr: + messages.append({"role": "user", "content": f"\n{tr}\n"}) + + # Current user message (live screen + most recent ) messages.append({"role": "user", "content": user_message}) - - # Apply prompt caching for OpenRouter (Gemini/Claude need explicit cache_control) - # Cache from system prompt through N-2 (all cleaned assistant messages) + + # Apply prompt caching for OpenRouter/Anthropic (explicit cache_control). # messages[-1] = current user message (dynamic, never cache) - # messages[-2] = last assistant message (action kept, not yet cleaned) - # messages[-3] = last cleaned assistant message (safe to cache up to here) - # Starts at step 3 (len >= 4): system + cleaned_step1 + last_step2 + user + # messages[-2] = most recent assistant step (full now -> trimmed next step) + # messages[-3] = last frozen turn (older tool_response/assistant) — safe to cache + # Starts once len >= 4. if self.llm_manager.get_provider_name() in ("openrouter", "anthropic") and len(messages) >= 4: cache_idx = len(messages) - 3 content = messages[cache_idx]["content"] @@ -685,16 +739,18 @@ def process_request(self, task: str) -> dict: # Reset consecutive JSON fail counter on success json_fail_count = 0 - # Save EXACTLY what agent memory contains (TRUE memory snapshot) - # Note: messages_for_api has previous responses with Step 1 replaced if applicable - # normalized_json is current response (with thinking) - self._save_conversation(messages_for_api, user_message, normalized_json, image_sent, step_number) - - # Remove thinking from response before adding to memory (saves tokens) - normalized_json_without_thinking = self._remove_thinking_from_response(normalized_json) - - # Add this assistant response (without thinking) to memory for next iteration - assistant_messages.append(f"\n{normalized_json_without_thinking}") + # Save a faithful snapshot of the EXACT payload sent this step (system + + # interleaved assistant/tool-response turns + current user message) plus + # this step's response — a true peek into agent memory. + self._save_conversation(messages, normalized_json, image_sent, step_number) + + # Store the FULL response (thinking/eval/action included). The sliding-window + # trim at send time keeps it full only while it's the most recent step, then + # trims thinking/eval/action once it's older. + assistant_messages.append(f"\n{normalized_json}") + # Keep tool_responses aligned 1:1 with assistant_messages; filled in after the + # action runs below (stays None for a step that takes no action). + tool_responses.append(None) # Format the response with emojis for console output (terminal: include action block) formatted_response = AgentResponseFormatter.format_response(normalized_json, include_action=True) @@ -766,7 +822,18 @@ def process_request(self, task: str) -> dict: # Store the action result as last_response last_response = json.dumps(action_result, indent=2) - + + # Preserve this step's tool result on the USER side: stash the + # (web-compacted) result in tool_responses, aligned with this + # step's assistant entry, so later iterations replay it as a + # user turn ("I clicked there -> result"). Web results are + # compacted to a scratchpad pointer by _tool_response_for_memory. + if tool_responses: + tool_responses[-1] = json.dumps( + self._tool_response_for_memory(action_result), + indent=2, ensure_ascii=False + ) + # Wait before next scan (default 3 seconds, unless wait action was used) wait_time = 3.0 # Default wait if action_result.get("tool") == "wait": diff --git a/Auto_Use/macOS_use/agent/main_driver/system_prompt.md b/Auto_Use/macOS_use/agent/main_driver/system_prompt.md index ea02e8b..853d3d9 100644 --- a/Auto_Use/macOS_use/agent/main_driver/system_prompt.md +++ b/Auto_Use/macOS_use/agent/main_driver/system_prompt.md @@ -77,7 +77,9 @@ Each step includes: 1. decision: Decision made based on images. 2. next_goal: Forward plan — the immediate step plus the next few anticipated steps. 3. memory: Key information stored. -4. action: Action performed. +4. action: Action performed that step. +*The most recent step also keeps its `thinking` and `eval`; older steps are trimmed to the fields above to save context. +*Each step's tool result follows it as a `` user turn — this is how you see what your action produced (e.g. click outcome with element_name, shell output). Web results are summarized there; the raw data is saved to . *Use tools only inside the action list.* @@ -166,7 +168,7 @@ Each step includes: *You must reason explicitly and systematically at every step in your thinking block. Work through the rules below as five labeled stages — OBSERVE → VERIFY → PROGRESS → PLAN → PREDICT — to successfully achieve the objective:* 1. Reason about to track progress and context toward . -2. Analyse the most recent "memory", "next_goal", and "action" in and clearly state what you previously planned and achieved (the "next_goal" lays out the immediate step plus the next 2-3 anticipated steps). +2. Analyse the most recent "memory", "next_goal", "action" and its "" in and clearly state what you previously planned and achieved (the "next_goal" lays out the immediate step plus the next 2-3 anticipated steps). 3. Analyse all the most relevant , , , , , and the screenshot to understand your current state. 4. Judge success/failure of the last action using as primary ground truth (not ). Feed your conclusion into "eval". 1. Example: you might have `"action": [{"input": {"74": "abc@gmail.com"}}]` with a success response in , even though inputting text actually failed. If the expected change is missing on screen, mark "eval" as FAIL and plan a recovery. diff --git a/Auto_Use/windows_use/agent/main_driver/service.py b/Auto_Use/windows_use/agent/main_driver/service.py index d10a1bd..9323400 100644 --- a/Auto_Use/windows_use/agent/main_driver/service.py +++ b/Auto_Use/windows_use/agent/main_driver/service.py @@ -56,7 +56,7 @@ def _compress_screenshot(base64_str: str, max_width: int = 1080, quality: int = def _cleanup_scratchpad(): """Clear all contents inside windows_use/scratchpad/ for a fresh start.""" # Clear scratchpad contents - scratchpad_dir = Path(__file__).parent.parent / "scratchpad" + scratchpad_dir = Path(__file__).parent.parent.parent / "scratchpad" if scratchpad_dir.exists(): for item in scratchpad_dir.iterdir(): if item.is_dir(): @@ -174,45 +174,48 @@ def _initialize_conversation_file(self): except Exception as e: print(f"Error initializing conversation file: {str(e)}") - def _save_conversation_snapshot(self, assistant_messages: list, user_message: str, current_assistant_response: str, image_sent: bool, interaction_count: int): - """Save a new numbered conversation file for this interaction - TRUE agent memory""" + def _save_conversation_snapshot(self, messages: list, current_assistant_response: str, image_sent: bool, interaction_count: int): + """Save a numbered conversation file rendering the EXACT payload sent this + step (system + interleaved assistant/user turns + current user message) plus + this step's freshly generated response — a faithful peek into agent memory.""" try: - # Create numbered filename: conversation_1.txt, conversation_2.txt, etc. conversation_file = self.conversation_dir / f"conversation_{interaction_count}.txt" - + + def _text(content): + # Cached messages carry content as a list of {type,text,...} blocks. + if isinstance(content, list): + return "\n".join(b.get("text", "") for b in content if isinstance(b, dict)) + return content if isinstance(content, str) else str(content) + with open(conversation_file, 'w', encoding='utf-8') as f: - # Write header + # Header f.write("=== CONVERSATION LOG ===\n") f.write(f"Session Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"Provider: {self.llm_manager.get_provider_name()}\n") f.write(f"Model: {self.llm_manager.get_model_name()}\n") f.write(f"Current Interaction: #{interaction_count}\n") f.write("=" * 60 + "\n\n") - - # Write system prompt - f.write("=== SYSTEM PROMPT ===\n") - f.write(self.system_prompt) - f.write("\n\n" + "=" * 60 + "\n\n") - - # Write all previous assistant messages (agent history) with step markers - for i, assistant_msg in enumerate(assistant_messages, 1): - f.write(f"INTERACTION #{i}\n") - f.write("=" * 60 + "\n") - f.write(f"ASSISTANT:\n\n{assistant_msg}\n\n") - f.write("\n") - - # Write current interaction (user message + current assistant response) - f.write(f"INTERACTION #{interaction_count}\n") - f.write("=" * 60 + "\n") - f.write(f"USER:\n{user_message}\n") + + # Render every message in the exact order it was sent to the model. + for m in messages: + role = str(m.get("role", "?")).upper() + if role == "SYSTEM": + f.write("=== SYSTEM PROMPT ===\n") + f.write(_text(m.get("content", ""))) + f.write("\n\n" + "=" * 60 + "\n\n") + else: + f.write(f"--- {role} ---\n") + f.write(_text(m.get("content", ""))) + f.write("\n\n") + if image_sent: - f.write("\n[Screenshot sent]\n") - f.write("\n") - - # Write current assistant response with step marker - f.write(f"ASSISTANT:\n\n{current_assistant_response}\n\n") + f.write("[Screenshot sent with the latest user message]\n\n") + + # This step's freshly generated response (the reply, not part of the request). + f.write(f"--- ASSISTANT (response · step {interaction_count}) ---\n") + f.write(current_assistant_response) f.write("\n") - + print(f"✓ Memory snapshot saved: conversation_{interaction_count}.txt") except Exception as e: print(f"Error saving conversation snapshot: {str(e)}") @@ -228,15 +231,17 @@ def _save_raw_response(self, raw_response: str, step_number: int): except Exception as e: print(f"⚠ Error saving raw response: {str(e)}") - def _save_conversation(self, assistant_messages: list, user_message: str, current_assistant_response: str, image_sent: bool, interaction_count: int): + def _save_conversation(self, messages: list, current_assistant_response: str, image_sent: bool, interaction_count: int): """Save conversation snapshot to file - simple and direct""" if self.save_conversation: - self._save_conversation_snapshot(assistant_messages, user_message, current_assistant_response, image_sent, interaction_count) + self._save_conversation_snapshot(messages, current_assistant_response, image_sent, interaction_count) def _read_todo_from_file(self) -> str: """Read the current todo list from scratchpad/todo/todo.md file""" try: - todo_file = Path(__file__).parent.parent / "scratchpad" / "todo" / "todo.md" + # Read from the task tracker's own file path (single source of truth — + # exactly where TaskTrackerService writes), so read can never drift from write. + todo_file = Path(self.controller.task_tracker.todo_file) if todo_file.exists(): with open(todo_file, 'r', encoding='utf-8') as f: return f.read().strip() @@ -249,7 +254,8 @@ def _read_todo_from_file(self) -> str: def _read_scratchpad_from_file(self) -> str: """Read the current scratchpad entries from scratchpad/milestone/milestone.md file""" try: - scratchpad_file = Path(__file__).parent.parent / "scratchpad" / "milestone" / "milestone.md" + # Read from the scratchpad service's own file path (single source of truth). + scratchpad_file = Path(self.controller.scratchpad_service.scratchpad_file) if scratchpad_file.exists(): with open(scratchpad_file, 'r', encoding='utf-8') as f: return f.read().strip() @@ -296,7 +302,52 @@ def _remove_thinking_from_response(self, response_json: str) -> str: return json.dumps(response_data, indent=2, ensure_ascii=False) except Exception: return response_json - + + def _tool_response_for_memory(self, action_result: dict) -> dict: + """Build the compact tool_response preserved in agent memory for a step. + + Only web-tool results are compacted: their raw text is already removed and + digested into the scratchpad, so memory keeps just the query + a pointer. + Web is compacted ONLY when it succeeded — a failed web call keeps its exact + response so the agent can see what went wrong. Every other result (shell + output, click/input results, etc.) is preserved verbatim. + """ + import copy + res = copy.deepcopy(action_result) + + def compact(entry): + if (isinstance(entry, dict) + and entry.get("tool") == "web" + and entry.get("status") == "success"): + return { + "action": "tool", + "tool": "web", + "query": entry.get("query", ""), + "message": "memory optimized — refer to scratchpad for the web result", + } + return entry + + if res.get("action") == "multiple" and "results" in res: + res["results"] = [compact(r) for r in res["results"]] + return res + return compact(res) + + def _trim_history_entry(self, entry: str) -> str: + """Trim an OLDER history step for context: drop 'thinking', 'eval', and + 'action' (keep decision/memory/next_goal). Only the most recent step is kept + full so the agent retains its latest reasoning; everything older is trimmed. + Handles the leading '' marker. + """ + m = re.match(r'(\n)(.*)', entry, re.DOTALL) + prefix, json_part = (m.group(1), m.group(2)) if m else ("", entry) + try: + data = json.loads(json_part) + for field in ("thinking", "eval", "action"): + data.pop(field, None) + return prefix + json.dumps(data, indent=2, ensure_ascii=False) + except Exception: + return entry + def process_request(self, task: str) -> dict: """Process a user request in an iterative loop until completion. @@ -312,6 +363,7 @@ def process_request(self, task: str) -> dict: last_response = None is_first_iteration = True assistant_messages = [] # Track all assistant responses for memory + tool_responses = [] # Per-step tool result, aligned 1:1 with assistant_messages; replayed as user turns cli_await_result = None # Stores cli_await result for next iteration's light message pending_web_response = None # Stores web tool response for light digest iteration json_fail_count = 0 # Track consecutive JSON parse failures (max 3 before exit) @@ -586,43 +638,45 @@ def process_request(self, task: str) -> dict: user_message += "\n\nAnnotated screenshot with bounding boxes" user_message += "\n\nPure vision first: decide which element to interact with from the screenshot alone, then refer to for its [id]." - # Create modified assistant messages for both API and saving - # Also strips 'action' from all middle entries (keeps only last entry's action) - messages_for_api = [] - if len(assistant_messages) > 0 and not is_first_iteration: - # Step 1: prepend user task + strip action (reinforces objective in context) - step1_msg = self._remove_action_from_response(assistant_messages[0]) - messages_for_api.append(f"\n{task}\n\n\n{step1_msg}") - - # Remaining entries except last: strip action to save tokens - for msg in assistant_messages[1:-1]: - messages_for_api.append(self._remove_action_from_response(msg)) - - # Last entry: keep action intact (needed for eval analysis) - if len(assistant_messages) > 1: - messages_for_api.append(assistant_messages[-1]) - else: - # Use original messages - messages_for_api = assistant_messages.copy() - - # Prepare messages for API - include all past assistant messages + # Build the API messages as a real interleaved dialogue: + # system, then for each past step: assistant(step) -> user(tool_response), + # and finally the current user message (live screen + ). + # + # Sliding-window trim: only the MOST RECENT past step keeps its full + # thinking/eval/action; every older step is trimmed to + # decision/memory/next_goal (_trim_history_entry), so the agent always sees + # its latest reasoning in full and just the outline of older steps. + # Tool results stay on the USER side (correct role -> clean eval, no schema + # leakage). The most recent step's result is NOT re-emitted here — it already + # rides in the current user message as . messages = [ {"role": "system", "content": self.system_prompt} ] - - # Add all assistant messages (with Step 1 replaced if applicable) - for assistant_msg in messages_for_api: - messages.append({"role": "assistant", "content": assistant_msg}) - # Add current user message + n_hist = len(assistant_messages) + for i, step_msg in enumerate(assistant_messages): + is_recent = (i == n_hist - 1) + content = step_msg if is_recent else self._trim_history_entry(step_msg) + # Reinforce the objective by prepending the task to step 1. + if i == 0 and not is_first_iteration: + content = f"\n{task}\n\n\n{content}" + messages.append({"role": "assistant", "content": content}) + + # Interleave this step's tool result as its own user turn — except the + # most recent, whose result is carried by the current user message below. + if not is_recent: + tr = tool_responses[i] if i < len(tool_responses) else None + if tr: + messages.append({"role": "user", "content": f"\n{tr}\n"}) + + # Current user message (live screen + most recent ) messages.append({"role": "user", "content": user_message}) - - # Apply prompt caching for OpenRouter (Gemini/Claude need explicit cache_control) - # Cache from system prompt through N-2 (all cleaned assistant messages) + + # Apply prompt caching for OpenRouter/Anthropic (explicit cache_control). # messages[-1] = current user message (dynamic, never cache) - # messages[-2] = last assistant message (action kept, not yet cleaned) - # messages[-3] = last cleaned assistant message (safe to cache up to here) - # Starts at step 3 (len >= 4): system + cleaned_step1 + last_step2 + user + # messages[-2] = most recent assistant step (full now -> trimmed next step) + # messages[-3] = last frozen turn (older tool_response/assistant) — safe to cache + # Starts once len >= 4. if self.llm_manager.get_provider_name() in ("openrouter", "anthropic") and len(messages) >= 4: cache_idx = len(messages) - 3 content = messages[cache_idx]["content"] @@ -680,16 +734,18 @@ def process_request(self, task: str) -> dict: # Reset consecutive JSON fail counter on success json_fail_count = 0 - # Save EXACTLY what agent memory contains (TRUE memory snapshot) - # Note: messages_for_api has previous responses with Step 1 replaced if applicable - # normalized_json is current response (with thinking) - self._save_conversation(messages_for_api, user_message, normalized_json, image_sent, step_number) - - # Remove thinking from response before adding to memory (saves tokens) - normalized_json_without_thinking = self._remove_thinking_from_response(normalized_json) - - # Add this assistant response (without thinking) to memory for next iteration - assistant_messages.append(f"\n{normalized_json_without_thinking}") + # Save a faithful snapshot of the EXACT payload sent this step (system + + # interleaved assistant/tool-response turns + current user message) plus + # this step's response — a true peek into agent memory. + self._save_conversation(messages, normalized_json, image_sent, step_number) + + # Store the FULL response (thinking/eval/action included). The sliding-window + # trim at send time keeps it full only while it's the most recent step, then + # trims thinking/eval/action once it's older. + assistant_messages.append(f"\n{normalized_json}") + # Keep tool_responses aligned 1:1 with assistant_messages; filled in after the + # action runs below (stays None for a step that takes no action). + tool_responses.append(None) # Format the response with emojis for console output (terminal: include action block) formatted_response = AgentResponseFormatter.format_response(normalized_json, include_action=True) @@ -761,7 +817,18 @@ def process_request(self, task: str) -> dict: # Store the action result as last_response last_response = json.dumps(action_result, indent=2) - + + # Preserve this step's tool result on the USER side: stash the + # (web-compacted) result in tool_responses, aligned with this + # step's assistant entry, so later iterations replay it as a + # user turn ("I clicked there -> result"). Web results are + # compacted to a scratchpad pointer by _tool_response_for_memory. + if tool_responses: + tool_responses[-1] = json.dumps( + self._tool_response_for_memory(action_result), + indent=2, ensure_ascii=False + ) + # Wait before next scan (default 3 seconds, unless wait action was used) wait_time = 3.0 # Default wait if action_result.get("tool") == "wait": diff --git a/Auto_Use/windows_use/agent/main_driver/system_prompt.md b/Auto_Use/windows_use/agent/main_driver/system_prompt.md index ba382bd..3dcdb41 100644 --- a/Auto_Use/windows_use/agent/main_driver/system_prompt.md +++ b/Auto_Use/windows_use/agent/main_driver/system_prompt.md @@ -74,7 +74,9 @@ Each step includes: 1. decision: Decision made based on images. 2. next_goal: Forward plan — the immediate step plus the next few anticipated steps. 3. memory: Key information stored. -4. action: Action performed. +4. action: Action performed that step. +*The most recent step also keeps its `thinking` and `eval`; older steps are trimmed to the fields above to save context. +*Each step's tool result follows it as a `` user turn — this is how you see what your action produced (e.g. click outcome with element_name, shell output). Web results are summarized there; the raw data is saved to . *Use tools only inside the action list.* @@ -151,7 +153,7 @@ Each step includes: *You must reason explicitly and systematically at every step in your thinking block. Work through the rules below as five labeled stages — OBSERVE → VERIFY → PROGRESS → PLAN → PREDICT — to successfully achieve the objective:* 1. Reason about to track progress and context toward . -2. Analyse the most recent "memory", "next_goal", and "action" in and clearly state what you previously planned and achieved (the "next_goal" lays out the immediate step plus the next 2-3 anticipated steps). +2. Analyse the most recent "memory", "next_goal", "action" and its "" in and clearly state what you previously planned and achieved (the "next_goal" lays out the immediate step plus the next 2-3 anticipated steps). 3. Analyse all the most relevant , , , , , and the screenshot to understand your current state. 4. Judge success/failure of the last action using as primary ground truth (not ). Feed your conclusion into "eval". 1. Example: you might have `"action": [{"input": {"74": "abc@gmail.com"}}]` with a success response in , even though inputting text actually failed. If the expected change is missing on screen, mark "eval" as FAIL and plan a recovery. diff --git a/main.py b/main.py index c1565db..3915e8b 100644 --- a/main.py +++ b/main.py @@ -33,7 +33,8 @@ # Your task here task = """ -Update the autouse codebase with two new providers: MinimaxM3 and Kimi 2.7 (via the OpenRouter provider). Wait for the code update to complete, then document today's progress in the notes. Next, search the internet for the current Nasdaq status and overall stock market performance. After that, download and install the Claude application on Mac. Finally, update the notes with a list of all completed tasks. Please execute these steps one by one. +one the and play anything on youtube. and make a google sheet of netflix 10 year revenue. + """ # Control conversation saving diff --git a/windows_binary_build (1).py b/windows_binary_build (1).py new file mode 100644 index 0000000..25fde09 --- /dev/null +++ b/windows_binary_build (1).py @@ -0,0 +1,1016 @@ +# Copyright 2026 Autouse AI — https://github.com/auto-use/Auto-Use +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# If you build on this project, please keep this header and credit +# Autouse AI (https://github.com/auto-use/Auto-Use) in forks and derivative works. +# A small attribution goes a long way toward a healthy open-source +# community — thank you for contributing. + +""" +Auto Use - Production Build Script (Nuitka) +============================================ +Compiles to native binary for code protection. This script also packs all +embedded resources (data files under Auto_Use/ and frontend/) into +_embedded_resources.py as the first step, so there's only one command to run. + +Requirements: + pip install nuitka ordered-set zstandard --break-system-packages + + Also requires a C compiler: + - Windows: Install Visual Studio Build Tools (MSVC) or MinGW64 + - Recommended: VS Build Tools 2022 with "Desktop development with C++" + +Run: python windows_binary_build.py + +Output: AutoUse_Setup.exe in project root +""" + +import os +import sys +import shutil +import base64 +import subprocess +from pathlib import Path + +# ============================================================================= +# CONFIGURATION +# ============================================================================= + +APP_NAME = "Auto Use" +APP_VERSION = "1.4" +APP_PUBLISHER = "Ashish Yadav" +APP_EXE_NAME = "AutoUse" +ENTRY_POINT = "app.py" + +# Code Signing Configuration +# For purchased certificate: Set path to your .pfx file and password +# For self-signed: Leave as None and the script will create one +CODE_SIGN_CERT_PATH = None # e.g., r"C:\certs\AutoUse.pfx" +CODE_SIGN_PASSWORD = None # Certificate password (if using .pfx) +CODE_SIGN_TIMESTAMP_URL = "http://timestamp.digicert.com" # Timestamp server + +# Inno Setup compiler path +INNO_SETUP_COMPILER = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" + +# Directories +ROOT_DIR = Path(__file__).parent.resolve() +BUILD_DIR = ROOT_DIR / "build" +DIST_DIR = ROOT_DIR / "dist" +OUTPUT_DIR = ROOT_DIR / "installer_output" + +# Embedded-resource packer config (see pack_embedded_resources below) +RESOURCE_EXTENSIONS = {'.md', '.json', '.html', '.css', '.js', '.png', '.ico', '.jpg', '.jpeg', '.gif', '.mp4'} +RESOURCE_FOLDERS = ['Auto_Use', 'frontend'] +RESOURCE_OUTPUT_FILE = '_embedded_resources.py' +API_KEY_FILE = os.path.join('Auto_Use', 'api_key', 'api_key.txt') + +# ============================================================================= +# HELPER FUNCTIONS +# ============================================================================= + +def print_step(step_num, message): + print(f"\n{'='*60}") + print(f" STEP {step_num}: {message}") + print(f"{'='*60}\n") + +def print_success(message): + print(f" [OK] {message}") + +def print_error(message): + print(f" [ERROR] {message}") + +def print_info(message): + print(f" [INFO] {message}") + +def pre_flight_checks(): + """Production readiness checks - runs before packing resources. + + NOTE: the old ensure_frontend_platform_sync step is gone. frontend/script.js + now picks the splash animation at runtime based on navigator.platform, and + frontend/index.html no longer hardcodes a splash src — so rewriting those + files at build time would actually corrupt the runtime-detection logic. + Both windows_animation.html and mac_animation.html are packed as-is. + """ + print_step("0a", "Pre-flight checks") + + issues_fixed = 0 + + if os.path.exists(API_KEY_FILE): + with open(API_KEY_FILE, 'r') as f: + lines = f.readlines() + + cleaned_lines = [] + changed = False + for line in lines: + if '=' in line: + tag = line.split('=', 1)[0] + cleaned = tag + '=\n' + if cleaned != line.rstrip('\n') + '\n': + changed = True + cleaned_lines.append(cleaned) + else: + cleaned_lines.append(line) + + if changed: + with open(API_KEY_FILE, 'w') as f: + f.writelines(cleaned_lines) + print_success("Stripped API keys from api_key.txt") + issues_fixed += 1 + else: + print_success("api_key.txt - already clean") + else: + print_info("api_key.txt not found (will be created on first run)") + + print_info(f"Pre-flight: {issues_fixed} issue(s) fixed") + + +def pack_embedded_resources(): + """Walk RESOURCE_FOLDERS and base64-encode all matching files into _embedded_resources.py. + + The compiled binary imports this generated module and serves files from it via + the patched `open()` and `serve_embedded_file` helpers in app.py. + """ + print_step("0b", "Packing embedded resources") + + resources = {} + total_size = 0 + + for folder in RESOURCE_FOLDERS: + folder_path = ROOT_DIR / folder + if not folder_path.exists(): + print_info(f"Folder not found, skipping: {folder}") + continue + + print_info(f"Scanning {folder}/") + folder_count = 0 + + for root, dirs, files in os.walk(folder_path): + # Exclude runtime/temp folders and cache + dirs[:] = [d for d in dirs if d not in ('__pycache__', '.git', 'venv', '.venv', 'scratchpad')] + + for filename in files: + ext = Path(filename).suffix.lower() + if ext not in RESOURCE_EXTENSIONS: + continue + + filepath = os.path.join(root, filename) + # Normalize to forward-slash relative path as the dict key + rel_path = os.path.relpath(filepath, ROOT_DIR).replace('\\', '/') + + try: + with open(filepath, 'rb') as f: + content = f.read() + resources[rel_path] = base64.b64encode(content).decode('ascii') + total_size += len(content) + folder_count += 1 + except Exception as e: + print_error(f"Failed to read {filepath}: {e}") + + print_info(f" Found {folder_count} files in {folder}/") + + if not resources: + print_error("No resources found to pack!") + return False + + output_path = ROOT_DIR / RESOURCE_OUTPUT_FILE + print_info(f"Writing {RESOURCE_OUTPUT_FILE}") + + with open(output_path, 'w', encoding='utf-8') as f: + f.write('"""\n') + f.write('Auto-generated embedded resources - DO NOT EDIT\n') + f.write(f'Contains {len(resources)} files ({total_size / 1024:.1f} KB total)\n') + f.write('"""\n\n') + f.write('RESOURCES = {\n') + + for path, data in sorted(resources.items()): + safe_path = path.replace('"', '\\"') + f.write(f' "{safe_path}":\n') + f.write(f' "{data}",\n') + + f.write('}\n') + + output_size = output_path.stat().st_size + print_success(f"Packed {len(resources)} files ({total_size / 1024:.1f} KB source, {output_size / 1024:.1f} KB base64)") + return True + + +def clean_build_directories(): + print_step(1, "Cleaning previous builds") + + for dir_path in [BUILD_DIR, DIST_DIR, OUTPUT_DIR]: + if dir_path.exists(): + shutil.rmtree(dir_path) + print_success(f"Removed {dir_path.name}/") + + # Clean Nuitka build artifacts + nuitka_build = ROOT_DIR / f"{ENTRY_POINT.replace('.py', '.build')}" + nuitka_dist = ROOT_DIR / f"{ENTRY_POINT.replace('.py', '.dist')}" + nuitka_onefile = ROOT_DIR / f"{ENTRY_POINT.replace('.py', '.onefile-build')}" + + for nuitka_dir in [nuitka_build, nuitka_dist, nuitka_onefile]: + if nuitka_dir.exists(): + shutil.rmtree(nuitka_dir) + print_success(f"Removed {nuitka_dir.name}/") + +def get_site_packages_path(): + """Find the site-packages directory that has our project's dependencies. + + Works whether running from a venv or global Python install. + Tries importing a known package first, falls back to site module. + """ + # Best method: derive from a known installed package + for probe in ('flask', 'PIL', 'numpy', 'requests'): + try: + mod = __import__(probe) + mod_file = getattr(mod, '__file__', None) + if mod_file: + candidate = Path(mod_file).parent.parent + if candidate.name == 'site-packages': + return candidate + except ImportError: + continue + + # Fallback: use site module + import site as _site + for sp in _site.getsitepackages(): + p = Path(sp) + if p.exists() and p.name == 'site-packages': + return p + + return None + + +def copy_third_party_packages(): + """Copy all third-party packages from site-packages into the Nuitka dist. + + Nuitka compiles only our proprietary code (Auto_Use.windows_use) to native binary. + Third-party packages are excluded from compilation (--nofollow-import-to) + to prevent MSVC OOM and speed up builds. This step copies them as regular + Python files so they're available at runtime. + """ + print_step("4a", "Copying third-party packages to dist") + + site_packages = get_site_packages_path() + if not site_packages or not site_packages.exists(): + print_error("Could not find site-packages directory!") + print_error("Make sure you run the build from the environment with all packages installed.") + return False + + dist_path = DIST_DIR / APP_EXE_NAME + if not dist_path.exists(): + print_error(f"Dist directory not found: {dist_path}") + return False + + print_info(f"Source: {site_packages}") + print_info(f"Target: {dist_path}") + + SKIP_NAMES = { + '__pycache__', 'pip', 'setuptools', 'pkg_resources', '_distutils_hack', + 'nuitka', 'ordered_set', 'zstandard', 'SCons', + 'Auto_Use', + # Packages compiled by Nuitka -- don't overwrite with .py from site-packages + 'pywinauto', 'PIL', 'numpy', 'requests', 'dotenv', 'keyboard', + 'flask', 'comtypes', 'interception', + # Transitive deps compiled via flask + 'werkzeug', 'jinja2', 'markupsafe', 'itsdangerous', 'click', 'blinker', + # Transitive deps compiled via requests + 'urllib3', 'certifi', 'charset_normalizer', 'idna', + } + SKIP_SUFFIXES = ('.dist-info', '.egg-info', '.egg-link', '.pth', '.pyc') + + copied_dirs = 0 + copied_files = 0 + merged = 0 + + for item in sorted(site_packages.iterdir()): + name = item.name + + if any(name.endswith(s) for s in SKIP_SUFFIXES): + continue + if name in SKIP_NAMES: + continue + if name.startswith('~') or name.startswith('.'): + continue + + dest = dist_path / name + + try: + if item.is_dir(): + if dest.exists(): + shutil.copytree( + item, dest, + ignore=shutil.ignore_patterns('__pycache__', '*.pyc'), + dirs_exist_ok=True, + ) + merged += 1 + else: + shutil.copytree( + item, dest, + ignore=shutil.ignore_patterns('__pycache__', '*.pyc'), + ) + copied_dirs += 1 + elif item.is_file(): + if not dest.exists(): + shutil.copy2(item, dest) + copied_files += 1 + except Exception as e: + print_error(f"Failed to copy {name}: {e}") + + print_success(f"Copied {copied_dirs} packages, {copied_files} files, merged {merged} existing") + + # Also copy .pth files that add directories to sys.path (e.g. pywin32.pth) + pth_count = 0 + for pth in site_packages.glob('*.pth'): + dest_pth = dist_path / pth.name + if not dest_pth.exists(): + shutil.copy2(pth, dest_pth) + pth_count += 1 + if pth_count: + print_info(f"Copied {pth_count} .pth files for path resolution") + + return True + + +def convert_png_to_ico(): + print_step(2, "Preparing application icon") + + png_path = ROOT_DIR / "Auto_Use" / "logo" / "auto_use.png" + ico_path = ROOT_DIR / "auto_use.ico" + + if not png_path.exists(): + print_error(f"Icon not found at {png_path}") + return None + + try: + from PIL import Image + img = Image.open(png_path) + # Order from largest to smallest - Windows uses first size as primary display + sizes = [(256, 256), (128, 128), (64, 64), (48, 48), (32, 32), (16, 16)] + icons = [] + + for size in sizes: + resized = img.resize(size, Image.Resampling.LANCZOS) + if resized.mode != 'RGBA': + resized = resized.convert('RGBA') + icons.append(resized) + + # Save with largest icon as primary + icons[0].save(ico_path, format='ICO', append_images=icons[1:]) + print_success(f"Created {ico_path.name}") + return ico_path + + except Exception as e: + print_error(f"Failed to convert icon: {e}") + return None + +def get_msvc_env(): + """Get environment variables from vcvarsall.bat for VS 2026 Community. + + Manually invoke vcvarsall.bat and capture the resulting environment so + Nuitka/Scons can reliably locate cl.exe even when auto-detection misses it. + """ + vcvarsall = r"C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvarsall.bat" + + if not Path(vcvarsall).exists(): + print_info("vcvarsall.bat not found, falling back to default environment") + return None + + print_info(f"Setting up MSVC environment from VS 2026 Community...") + + try: + # Run vcvarsall.bat and capture the resulting environment + result = subprocess.run( + f'"{vcvarsall}" x64 && set', + capture_output=True, text=True, shell=True + ) + + if result.returncode != 0: + print_error(f"vcvarsall.bat failed: {result.stderr}") + return None + + # Parse environment variables from output + env = {} + for line in result.stdout.splitlines(): + if '=' in line: + key, _, value = line.partition('=') + env[key] = value + + print_success("MSVC environment configured") + return env + + except Exception as e: + print_error(f"Failed to set up MSVC environment: {e}") + return None + +def run_nuitka(icon_path): + print_step(3, "Compiling to native binary with Nuitka") + + # Create dist directory + DIST_DIR.mkdir(exist_ok=True) + + # Get MSVC environment (needed for VS 2026 Insiders which Nuitka can't auto-detect) + msvc_env = get_msvc_env() + + cmd = [ + sys.executable, "-m", "nuitka", + + # Output settings + "--standalone", # Create standalone distribution + "--output-dir=" + str(DIST_DIR), # Output directory + "--output-filename=" + APP_EXE_NAME, # Output executable name + + # Windows-specific + "--windows-console-mode=disable", # Hide console window (GUI app) + "--windows-company-name=" + APP_PUBLISHER, + "--windows-product-name=" + APP_NAME, + "--windows-file-version=" + APP_VERSION, + "--windows-product-version=" + APP_VERSION, + + # MSVC compiler (required for Python 3.13 - MinGW64 not supported) + # Environment from vcvarsall.bat is passed to subprocess so Nuitka finds cl.exe + "--msvc=latest", + + # Auto-accept download prompts (Dependency Walker, etc.) + "--assume-yes-for-downloads", + + # Code protection / optimization + "--lto=yes", # Link Time Optimization + "--jobs=2", # Reduced from 4 to prevent MSVC OOM + + # Allow importing modules excluded via --nofollow-import-to at runtime. + # We copy them as regular Python files in the post-build step. + "--no-deployment-flag=excluded-module-usage", + ] + + # Add icon if available + if icon_path and icon_path.exists(): + cmd.append(f"--windows-icon-from-ico={icon_path}") + + # NOTE: frontend/ and Auto_Use/windows_use/sandbox/terminal/ are now embedded in _embedded_resources.py + # and served via Flask routes - no need to include as data directories + # This completely hides all folder structure from the user + print_info("UI files embedded in binary (frontend/, terminal/)") + + # Create empty scratchpad folder structure post-build (done by app.py at runtime) + + # ========================================================================= + # COMPILE TO NATIVE BINARY + # Everything here is compiled by Nuitka to C → native binary. + # This was the working config before google-genai was added. + # ========================================================================= + include_packages = [ + "pywinauto", + "pywinauto.controls", + "pywinauto.controls.uiawrapper", + "PIL", + "numpy", + "requests", + "dotenv", + "keyboard", + "flask", + # Proprietary code (Auto_Use.windows_use) + "Auto_Use", + "Auto_Use.windows_use", + "Auto_Use.windows_use.llm_provider.openrouter.view", + "Auto_Use.windows_use.llm_provider.groq.view", + "Auto_Use.windows_use.llm_provider.openai.view", + "Auto_Use.windows_use.llm_provider.anthropic.view", + "Auto_Use.windows_use.llm_provider.google.view", + ] + + for package in include_packages: + cmd.append(f"--include-package={package}") + + include_modules = [ + "_embedded_resources", + "interception", + "comtypes", + "comtypes.client", + "comtypes.stream", + "win32api", + "win32con", + "win32gui", + "pydoc", # needed by scipy._lib._docscrape at runtime + ] + + for module in include_modules: + cmd.append(f"--include-module={module}") + + # ========================================================================= + # EXCLUDE FROM C COMPILATION (copied as Python by post-build step) + # + # Only packages that cause MSVC OOM or are open-source LLM SDKs. + # When adding new large packages in the future (e.g. Azure SDK), + # just add them here -- the post-build step copies them automatically. + # ========================================================================= + nofollow_third_party = [ + # Entire google namespace (genai, cloud, auth, api_core, protobuf) + # Must exclude as one unit to avoid namespace package conflicts + "google", + "aiohttp", + "grpc", + "protobuf", + "scipy", + "networkx", + + # LLM provider SDKs (open-source, no need to compile) + "openai", + "anthropic", + "groq", + + # Never needed + "tkinter", + "matplotlib", + "pytest", + "setuptools", + ] + + for pkg in nofollow_third_party: + cmd.append(f"--nofollow-import-to={pkg}") + + # NOTE: Auto_Use is NOT copied as data - all resources are embedded in binary + # This completely hides folder structure (no agent/, controller/, tree/ visible) + + # Add entry point + cmd.append(str(ROOT_DIR / ENTRY_POINT)) + + print_info("Running Nuitka (this may take 5-15 minutes on first build)...") + print_info("Command: " + " ".join(cmd[:10]) + " ...") + + try: + # Run Nuitka with real-time output, using MSVC environment if available + process = subprocess.Popen( + cmd, + cwd=ROOT_DIR, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + env=msvc_env # Pass MSVC environment so Nuitka/Scons can find cl.exe + ) + + # Stream all output in real-time (no filtering, so we can see errors/prompts) + for line in process.stdout: + line = line.rstrip() + if line: + print(f" {line}") + + process.wait() + + if process.returncode != 0: + print_error(f"Nuitka failed with return code {process.returncode}") + return False + + print_success("Nuitka compilation completed successfully") + return True + + except FileNotFoundError: + print_error("Nuitka not found! Install with: pip install nuitka ordered-set zstandard") + return False + except Exception as e: + print_error(f"Nuitka failed: {e}") + return False + +def organize_dist(): + """Organize Nuitka output to match expected structure for Inno Setup""" + print_step(4, "Organizing distribution files") + + # Nuitka creates: dist/app.dist/ containing the executable and dependencies + nuitka_dist = DIST_DIR / f"{ENTRY_POINT.replace('.py', '.dist')}" + final_dist = DIST_DIR / APP_EXE_NAME + + if not nuitka_dist.exists(): + # Check alternative location + nuitka_dist = ROOT_DIR / f"{ENTRY_POINT.replace('.py', '.dist')}" + + if not nuitka_dist.exists(): + print_error(f"Nuitka output not found at expected locations") + # List what's in dist + if DIST_DIR.exists(): + print_info(f"Contents of {DIST_DIR}:") + for item in DIST_DIR.iterdir(): + print_info(f" - {item.name}") + return False + + # Rename/move to expected location + if final_dist.exists(): + shutil.rmtree(final_dist) + + shutil.move(str(nuitka_dist), str(final_dist)) + + # Rename the executable if needed + old_exe = final_dist / f"{ENTRY_POINT.replace('.py', '.exe')}" + new_exe = final_dist / f"{APP_EXE_NAME}.exe" + + if old_exe.exists() and old_exe != new_exe: + old_exe.rename(new_exe) + + print_success(f"Distribution organized at {final_dist}") + + # List final contents + exe_count = len(list(final_dist.glob("*.exe"))) + dll_count = len(list(final_dist.glob("*.dll"))) + pyd_count = len(list(final_dist.glob("**/*.pyd"))) + print_info(f"Contents: {exe_count} exe, {dll_count} dll, {pyd_count} pyd files") + + return True + +def create_self_signed_cert(): + """Create a self-signed certificate for code signing if none exists""" + cert_path = ROOT_DIR / "certs" + pfx_path = cert_path / "AutoUse.pfx" + + if pfx_path.exists(): + print_info("Using existing certificate") + return pfx_path, "AutoUse123" # Default password for self-signed + + print_info("Creating self-signed certificate...") + cert_path.mkdir(exist_ok=True) + + # PowerShell command to create self-signed certificate + ps_script = f''' +$cert = New-SelfSignedCertificate ` + -Type CodeSigningCert ` + -Subject "CN={APP_PUBLISHER}, O={APP_NAME}" ` + -KeyAlgorithm RSA ` + -KeyLength 2048 ` + -HashAlgorithm SHA256 ` + -CertStoreLocation Cert:\\CurrentUser\\My ` + -NotAfter (Get-Date).AddYears(5) + +$pwd = ConvertTo-SecureString -String "AutoUse123" -Force -AsPlainText +Export-PfxCertificate -Cert $cert -FilePath "{pfx_path}" -Password $pwd + +# Also export to trusted root for local testing (optional) +$rootStore = New-Object System.Security.Cryptography.X509Certificates.X509Store("Root", "CurrentUser") +$rootStore.Open("ReadWrite") +$rootStore.Add($cert) +$rootStore.Close() + +Write-Host "Certificate created and exported to {pfx_path}" +Write-Host "Thumbprint: $($cert.Thumbprint)" +''' + + try: + result = subprocess.run( + ["powershell", "-ExecutionPolicy", "Bypass", "-Command", ps_script], + capture_output=True, + text=True + ) + + if result.returncode == 0 and pfx_path.exists(): + print_success(f"Self-signed certificate created: {pfx_path}") + return pfx_path, "AutoUse123" + else: + print_error(f"Failed to create certificate: {result.stderr}") + return None, None + + except Exception as e: + print_error(f"Certificate creation failed: {e}") + return None, None + +def sign_executable(exe_path, cert_path=None, password=None): + """Sign an executable with a code signing certificate""" + + if not exe_path.exists(): + print_error(f"Executable not found: {exe_path}") + return False + + # Determine certificate to use + if cert_path and Path(cert_path).exists(): + pfx_path = Path(cert_path) + pfx_password = password + elif CODE_SIGN_CERT_PATH and Path(CODE_SIGN_CERT_PATH).exists(): + pfx_path = Path(CODE_SIGN_CERT_PATH) + pfx_password = CODE_SIGN_PASSWORD + else: + # Create self-signed certificate + pfx_path, pfx_password = create_self_signed_cert() + if not pfx_path: + print_error("No certificate available for signing") + return False + + # Find signtool.exe + signtool_paths = [ + r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe", + r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64\signtool.exe", + r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe", + r"C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe", + ] + + signtool = None + for path in signtool_paths: + if Path(path).exists(): + signtool = path + break + + if not signtool: + # Try to find it via where command + try: + result = subprocess.run(["where", "signtool"], capture_output=True, text=True) + if result.returncode == 0: + signtool = result.stdout.strip().split('\n')[0] + except: + pass + + if not signtool: + print_error("signtool.exe not found!") + print_info("Install Windows SDK or Visual Studio with Windows SDK component") + print_info("Download: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/") + return False + + # Sign the executable + cmd = [ + signtool, + "sign", + "/f", str(pfx_path), + "/p", pfx_password, + "/fd", "SHA256", + "/tr", CODE_SIGN_TIMESTAMP_URL, + "/td", "SHA256", + "/d", APP_NAME, + str(exe_path) + ] + + try: + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + print_success(f"Signed: {exe_path.name}") + return True + else: + print_error(f"Signing failed: {result.stderr}") + return False + + except Exception as e: + print_error(f"Signing error: {e}") + return False + +def sign_all_executables(): + """Sign all executables in the distribution""" + print_step("4b", "Code signing executables") + + dist_path = DIST_DIR / APP_EXE_NAME + + if not dist_path.exists(): + print_error("Distribution directory not found") + return False + + # Sign main executable + main_exe = dist_path / f"{APP_EXE_NAME}.exe" + if main_exe.exists(): + if not sign_executable(main_exe): + print_info("Continuing without signature (UAC will show 'Unknown Publisher')") + + # Optionally sign DLLs (usually not necessary) + # for dll in dist_path.glob("*.dll"): + # sign_executable(dll) + + return True + +def create_inno_setup_script(): + print_step(5, "Creating Inno Setup script") + + dist_path = str(DIST_DIR / APP_EXE_NAME).replace("\\", "\\\\") + output_path = str(OUTPUT_DIR).replace("\\", "\\\\") + ico_path = str(ROOT_DIR / "auto_use.ico").replace("\\", "\\\\") + license_path = str(ROOT_DIR / "LICENSE").replace("\\", "\\\\") + interception_installer = str(ROOT_DIR / "Interception" / "command line installer" / "install-interception.exe").replace("\\", "\\\\") + + iss_content = f'''; Auto Use - Inno Setup Script (Nuitka Build) + +#define MyAppName "{APP_NAME}" +#define MyAppVersion "{APP_VERSION}" +#define MyAppPublisher "{APP_PUBLISHER}" +#define MyAppExeName "{APP_EXE_NAME}.exe" + +[Setup] +AppId={{{{8F3E9A5B-2C1D-4E7F-A8B9-1234567890AB}}}} +AppName={{#MyAppName}} +AppVersion={{#MyAppVersion}} +AppPublisher={{#MyAppPublisher}} +DefaultDirName={{localappdata}}\\{{#MyAppName}} +DefaultGroupName={{#MyAppName}} +AllowNoIcons=yes +OutputDir={output_path} +OutputBaseFilename=AutoUse_Setup +SetupIconFile={ico_path} +UninstallDisplayIcon={{app}}\\{{#MyAppExeName}} +Compression=lzma2/max +SolidCompression=yes +PrivilegesRequired=admin +MinVersion=10.0 +WizardStyle=modern +LicenseFile={license_path} +AlwaysRestart=yes + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional shortcuts:"; Flags: checkedonce + +[Files] +Source: "{dist_path}\\*"; DestDir: "{{app}}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{interception_installer}"; DestDir: "{{app}}\\InterceptionDriver"; Flags: ignoreversion + +[Icons] +Name: "{{group}}\\{{#MyAppName}}"; Filename: "{{app}}\\{{#MyAppExeName}}"; WorkingDir: "{{app}}" +Name: "{{group}}\\Uninstall {{#MyAppName}}"; Filename: "{{uninstallexe}}" +Name: "{{commondesktop}}\\{{#MyAppName}}"; Filename: "{{app}}\\{{#MyAppExeName}}"; WorkingDir: "{{app}}"; Tasks: desktopicon + +[Run] +Filename: "{{app}}\\InterceptionDriver\\install-interception.exe"; Parameters: "/install"; Flags: runhidden waituntilterminated; StatusMsg: "Installing Interception driver..." + +[UninstallRun] +Filename: "{{app}}\\InterceptionDriver\\install-interception.exe"; Parameters: "/uninstall"; Flags: runhidden waituntilterminated; RunOnceId: "InterceptionUninstall" + +[UninstallDelete] +Type: filesandordirs; Name: "{{app}}" + +[Messages] +FinishedLabel=Setup has finished installing [name] on your computer.%n%nIMPORTANT: A RESTART IS REQUIRED for the Auto Use driver to work properly.%n%nClick Finish to restart your computer now. + +[Code] +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var + ResultCode: Integer; +begin + if CurUninstallStep = usPostUninstall then + begin + DeleteFile(ExpandConstant('{{sys}}\\drivers\\keyboard.sys')); + DeleteFile(ExpandConstant('{{sys}}\\drivers\\mouse.sys')); + + if MsgBox('Uninstallation complete.' + #13#10 + #13#10 + + 'A restart is REQUIRED to fully remove the Auto Use driver.' + #13#10 + #13#10 + + 'Restart now?', + mbConfirmation, MB_YESNO) = IDYES then + begin + Exec('shutdown.exe', '/r /t 5 /c "Restarting to complete Auto Use uninstallation..."', '', SW_HIDE, ewNoWait, ResultCode); + end; + end; +end; + +function NeedRestart(): Boolean; +begin + Result := True; +end; +''' + + iss_path = ROOT_DIR / "installer.iss" + with open(iss_path, 'w', encoding='utf-8') as f: + f.write(iss_content) + + print_success(f"Created {iss_path.name}") + return iss_path + +def run_inno_setup(iss_path): + print_step(6, "Compiling installer with Inno Setup") + + if not Path(INNO_SETUP_COMPILER).exists(): + print_error(f"Inno Setup not found at: {INNO_SETUP_COMPILER}") + return False + + OUTPUT_DIR.mkdir(exist_ok=True) + + try: + result = subprocess.run( + [INNO_SETUP_COMPILER, str(iss_path)], + check=True, + capture_output=True, + text=True + ) + print_success("Inno Setup completed successfully") + return True + except subprocess.CalledProcessError as e: + print_error(f"Inno Setup failed!") + if e.stdout: + print_error(f"STDOUT: {e.stdout}") + if e.stderr: + print_error(f"STDERR: {e.stderr}") + return False + +def move_final_installer(): + print_step(7, "Finalizing") + + installer_path = OUTPUT_DIR / "AutoUse_Setup.exe" + final_path = ROOT_DIR / "AutoUse_Setup.exe" + + if installer_path.exists(): + if final_path.exists(): + final_path.unlink() + shutil.move(str(installer_path), str(final_path)) + size_mb = final_path.stat().st_size / (1024 * 1024) + print_success(f"Installer ready: {final_path.name} ({size_mb:.1f} MB)") + return final_path + else: + print_error("Installer not found") + return None + +def cleanup(): + """Clean up build artifacts""" + for dir_path in [BUILD_DIR, OUTPUT_DIR]: + if dir_path.exists(): + shutil.rmtree(dir_path) + + # Clean Nuitka build artifacts (keep dist for debugging if needed) + nuitka_build = ROOT_DIR / f"{ENTRY_POINT.replace('.py', '.build')}" + nuitka_onefile = ROOT_DIR / f"{ENTRY_POINT.replace('.py', '.onefile-build')}" + + for nuitka_dir in [nuitka_build, nuitka_onefile]: + if nuitka_dir.exists(): + shutil.rmtree(nuitka_dir) + + # Remove installer script + iss_file = ROOT_DIR / "installer.iss" + if iss_file.exists(): + iss_file.unlink() + + print_success("Cleanup complete") + +# ============================================================================= +# MAIN +# ============================================================================= + +def main(): + print("\n" + "="*60) + print(" AUTO USE - PRODUCTION BUILD (NUITKA)") + print(" Native Binary Compilation for Code Protection") + print("="*60) + + # Step 0: Pre-flight checks + pack embedded resources + pre_flight_checks() + if not pack_embedded_resources(): + print_error("Resource packing failed!") + sys.exit(1) + + # Step 1: Clean + clean_build_directories() + + # Step 2: Icon + icon_path = convert_png_to_ico() + + # Step 3: Nuitka compilation + if not run_nuitka(icon_path): + print_error("Nuitka compilation failed!") + sys.exit(1) + + # Step 4: Organize distribution + if not organize_dist(): + print_error("Failed to organize distribution!") + sys.exit(1) + + # Step 4a: Copy third-party packages into dist (excluded from Nuitka compilation) + if not copy_third_party_packages(): + print_error("Failed to copy third-party packages!") + sys.exit(1) + + # Step 4b: Code sign executables + sign_all_executables() + + # Step 5: Create Inno Setup script + iss_path = create_inno_setup_script() + + # Step 6: Run Inno Setup + if not run_inno_setup(iss_path): + print_error("Inno Setup failed!") + sys.exit(1) + + # Step 7: Move final installer + final_installer = move_final_installer() + + # Step 7b: Sign the installer + if final_installer: + print_step("7b", "Signing installer") + sign_executable(final_installer) + + if final_installer: + cleanup() + print("\n" + "="*60) + print(" [OK] BUILD SUCCESSFUL!") + print(f" Output: {final_installer.name}") + print(" ") + print(" Code Protection: Native binary (C compiled)") + print(" Reverse Engineering: Significantly harder") + print(f" Publisher: {APP_PUBLISHER}") + print(" ") + print(" NOTE: Self-signed cert shows your name in UAC but") + print(" still shows a warning. For full trust with no warning,") + print(" purchase a code signing certificate (~$200-500/year).") + print("="*60 + "\n") + else: + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file From f9d3d9a620578e4cfcbcc154955909b15357fd98 Mon Sep 17 00:00:00 2001 From: FunctionFreak Date: Thu, 18 Jun 2026 17:55:54 +0100 Subject: [PATCH 4/5] Delete windows_binary_build (1).py --- windows_binary_build (1).py | 1016 ----------------------------------- 1 file changed, 1016 deletions(-) delete mode 100644 windows_binary_build (1).py diff --git a/windows_binary_build (1).py b/windows_binary_build (1).py deleted file mode 100644 index 25fde09..0000000 --- a/windows_binary_build (1).py +++ /dev/null @@ -1,1016 +0,0 @@ -# Copyright 2026 Autouse AI — https://github.com/auto-use/Auto-Use -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# If you build on this project, please keep this header and credit -# Autouse AI (https://github.com/auto-use/Auto-Use) in forks and derivative works. -# A small attribution goes a long way toward a healthy open-source -# community — thank you for contributing. - -""" -Auto Use - Production Build Script (Nuitka) -============================================ -Compiles to native binary for code protection. This script also packs all -embedded resources (data files under Auto_Use/ and frontend/) into -_embedded_resources.py as the first step, so there's only one command to run. - -Requirements: - pip install nuitka ordered-set zstandard --break-system-packages - - Also requires a C compiler: - - Windows: Install Visual Studio Build Tools (MSVC) or MinGW64 - - Recommended: VS Build Tools 2022 with "Desktop development with C++" - -Run: python windows_binary_build.py - -Output: AutoUse_Setup.exe in project root -""" - -import os -import sys -import shutil -import base64 -import subprocess -from pathlib import Path - -# ============================================================================= -# CONFIGURATION -# ============================================================================= - -APP_NAME = "Auto Use" -APP_VERSION = "1.4" -APP_PUBLISHER = "Ashish Yadav" -APP_EXE_NAME = "AutoUse" -ENTRY_POINT = "app.py" - -# Code Signing Configuration -# For purchased certificate: Set path to your .pfx file and password -# For self-signed: Leave as None and the script will create one -CODE_SIGN_CERT_PATH = None # e.g., r"C:\certs\AutoUse.pfx" -CODE_SIGN_PASSWORD = None # Certificate password (if using .pfx) -CODE_SIGN_TIMESTAMP_URL = "http://timestamp.digicert.com" # Timestamp server - -# Inno Setup compiler path -INNO_SETUP_COMPILER = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" - -# Directories -ROOT_DIR = Path(__file__).parent.resolve() -BUILD_DIR = ROOT_DIR / "build" -DIST_DIR = ROOT_DIR / "dist" -OUTPUT_DIR = ROOT_DIR / "installer_output" - -# Embedded-resource packer config (see pack_embedded_resources below) -RESOURCE_EXTENSIONS = {'.md', '.json', '.html', '.css', '.js', '.png', '.ico', '.jpg', '.jpeg', '.gif', '.mp4'} -RESOURCE_FOLDERS = ['Auto_Use', 'frontend'] -RESOURCE_OUTPUT_FILE = '_embedded_resources.py' -API_KEY_FILE = os.path.join('Auto_Use', 'api_key', 'api_key.txt') - -# ============================================================================= -# HELPER FUNCTIONS -# ============================================================================= - -def print_step(step_num, message): - print(f"\n{'='*60}") - print(f" STEP {step_num}: {message}") - print(f"{'='*60}\n") - -def print_success(message): - print(f" [OK] {message}") - -def print_error(message): - print(f" [ERROR] {message}") - -def print_info(message): - print(f" [INFO] {message}") - -def pre_flight_checks(): - """Production readiness checks - runs before packing resources. - - NOTE: the old ensure_frontend_platform_sync step is gone. frontend/script.js - now picks the splash animation at runtime based on navigator.platform, and - frontend/index.html no longer hardcodes a splash src — so rewriting those - files at build time would actually corrupt the runtime-detection logic. - Both windows_animation.html and mac_animation.html are packed as-is. - """ - print_step("0a", "Pre-flight checks") - - issues_fixed = 0 - - if os.path.exists(API_KEY_FILE): - with open(API_KEY_FILE, 'r') as f: - lines = f.readlines() - - cleaned_lines = [] - changed = False - for line in lines: - if '=' in line: - tag = line.split('=', 1)[0] - cleaned = tag + '=\n' - if cleaned != line.rstrip('\n') + '\n': - changed = True - cleaned_lines.append(cleaned) - else: - cleaned_lines.append(line) - - if changed: - with open(API_KEY_FILE, 'w') as f: - f.writelines(cleaned_lines) - print_success("Stripped API keys from api_key.txt") - issues_fixed += 1 - else: - print_success("api_key.txt - already clean") - else: - print_info("api_key.txt not found (will be created on first run)") - - print_info(f"Pre-flight: {issues_fixed} issue(s) fixed") - - -def pack_embedded_resources(): - """Walk RESOURCE_FOLDERS and base64-encode all matching files into _embedded_resources.py. - - The compiled binary imports this generated module and serves files from it via - the patched `open()` and `serve_embedded_file` helpers in app.py. - """ - print_step("0b", "Packing embedded resources") - - resources = {} - total_size = 0 - - for folder in RESOURCE_FOLDERS: - folder_path = ROOT_DIR / folder - if not folder_path.exists(): - print_info(f"Folder not found, skipping: {folder}") - continue - - print_info(f"Scanning {folder}/") - folder_count = 0 - - for root, dirs, files in os.walk(folder_path): - # Exclude runtime/temp folders and cache - dirs[:] = [d for d in dirs if d not in ('__pycache__', '.git', 'venv', '.venv', 'scratchpad')] - - for filename in files: - ext = Path(filename).suffix.lower() - if ext not in RESOURCE_EXTENSIONS: - continue - - filepath = os.path.join(root, filename) - # Normalize to forward-slash relative path as the dict key - rel_path = os.path.relpath(filepath, ROOT_DIR).replace('\\', '/') - - try: - with open(filepath, 'rb') as f: - content = f.read() - resources[rel_path] = base64.b64encode(content).decode('ascii') - total_size += len(content) - folder_count += 1 - except Exception as e: - print_error(f"Failed to read {filepath}: {e}") - - print_info(f" Found {folder_count} files in {folder}/") - - if not resources: - print_error("No resources found to pack!") - return False - - output_path = ROOT_DIR / RESOURCE_OUTPUT_FILE - print_info(f"Writing {RESOURCE_OUTPUT_FILE}") - - with open(output_path, 'w', encoding='utf-8') as f: - f.write('"""\n') - f.write('Auto-generated embedded resources - DO NOT EDIT\n') - f.write(f'Contains {len(resources)} files ({total_size / 1024:.1f} KB total)\n') - f.write('"""\n\n') - f.write('RESOURCES = {\n') - - for path, data in sorted(resources.items()): - safe_path = path.replace('"', '\\"') - f.write(f' "{safe_path}":\n') - f.write(f' "{data}",\n') - - f.write('}\n') - - output_size = output_path.stat().st_size - print_success(f"Packed {len(resources)} files ({total_size / 1024:.1f} KB source, {output_size / 1024:.1f} KB base64)") - return True - - -def clean_build_directories(): - print_step(1, "Cleaning previous builds") - - for dir_path in [BUILD_DIR, DIST_DIR, OUTPUT_DIR]: - if dir_path.exists(): - shutil.rmtree(dir_path) - print_success(f"Removed {dir_path.name}/") - - # Clean Nuitka build artifacts - nuitka_build = ROOT_DIR / f"{ENTRY_POINT.replace('.py', '.build')}" - nuitka_dist = ROOT_DIR / f"{ENTRY_POINT.replace('.py', '.dist')}" - nuitka_onefile = ROOT_DIR / f"{ENTRY_POINT.replace('.py', '.onefile-build')}" - - for nuitka_dir in [nuitka_build, nuitka_dist, nuitka_onefile]: - if nuitka_dir.exists(): - shutil.rmtree(nuitka_dir) - print_success(f"Removed {nuitka_dir.name}/") - -def get_site_packages_path(): - """Find the site-packages directory that has our project's dependencies. - - Works whether running from a venv or global Python install. - Tries importing a known package first, falls back to site module. - """ - # Best method: derive from a known installed package - for probe in ('flask', 'PIL', 'numpy', 'requests'): - try: - mod = __import__(probe) - mod_file = getattr(mod, '__file__', None) - if mod_file: - candidate = Path(mod_file).parent.parent - if candidate.name == 'site-packages': - return candidate - except ImportError: - continue - - # Fallback: use site module - import site as _site - for sp in _site.getsitepackages(): - p = Path(sp) - if p.exists() and p.name == 'site-packages': - return p - - return None - - -def copy_third_party_packages(): - """Copy all third-party packages from site-packages into the Nuitka dist. - - Nuitka compiles only our proprietary code (Auto_Use.windows_use) to native binary. - Third-party packages are excluded from compilation (--nofollow-import-to) - to prevent MSVC OOM and speed up builds. This step copies them as regular - Python files so they're available at runtime. - """ - print_step("4a", "Copying third-party packages to dist") - - site_packages = get_site_packages_path() - if not site_packages or not site_packages.exists(): - print_error("Could not find site-packages directory!") - print_error("Make sure you run the build from the environment with all packages installed.") - return False - - dist_path = DIST_DIR / APP_EXE_NAME - if not dist_path.exists(): - print_error(f"Dist directory not found: {dist_path}") - return False - - print_info(f"Source: {site_packages}") - print_info(f"Target: {dist_path}") - - SKIP_NAMES = { - '__pycache__', 'pip', 'setuptools', 'pkg_resources', '_distutils_hack', - 'nuitka', 'ordered_set', 'zstandard', 'SCons', - 'Auto_Use', - # Packages compiled by Nuitka -- don't overwrite with .py from site-packages - 'pywinauto', 'PIL', 'numpy', 'requests', 'dotenv', 'keyboard', - 'flask', 'comtypes', 'interception', - # Transitive deps compiled via flask - 'werkzeug', 'jinja2', 'markupsafe', 'itsdangerous', 'click', 'blinker', - # Transitive deps compiled via requests - 'urllib3', 'certifi', 'charset_normalizer', 'idna', - } - SKIP_SUFFIXES = ('.dist-info', '.egg-info', '.egg-link', '.pth', '.pyc') - - copied_dirs = 0 - copied_files = 0 - merged = 0 - - for item in sorted(site_packages.iterdir()): - name = item.name - - if any(name.endswith(s) for s in SKIP_SUFFIXES): - continue - if name in SKIP_NAMES: - continue - if name.startswith('~') or name.startswith('.'): - continue - - dest = dist_path / name - - try: - if item.is_dir(): - if dest.exists(): - shutil.copytree( - item, dest, - ignore=shutil.ignore_patterns('__pycache__', '*.pyc'), - dirs_exist_ok=True, - ) - merged += 1 - else: - shutil.copytree( - item, dest, - ignore=shutil.ignore_patterns('__pycache__', '*.pyc'), - ) - copied_dirs += 1 - elif item.is_file(): - if not dest.exists(): - shutil.copy2(item, dest) - copied_files += 1 - except Exception as e: - print_error(f"Failed to copy {name}: {e}") - - print_success(f"Copied {copied_dirs} packages, {copied_files} files, merged {merged} existing") - - # Also copy .pth files that add directories to sys.path (e.g. pywin32.pth) - pth_count = 0 - for pth in site_packages.glob('*.pth'): - dest_pth = dist_path / pth.name - if not dest_pth.exists(): - shutil.copy2(pth, dest_pth) - pth_count += 1 - if pth_count: - print_info(f"Copied {pth_count} .pth files for path resolution") - - return True - - -def convert_png_to_ico(): - print_step(2, "Preparing application icon") - - png_path = ROOT_DIR / "Auto_Use" / "logo" / "auto_use.png" - ico_path = ROOT_DIR / "auto_use.ico" - - if not png_path.exists(): - print_error(f"Icon not found at {png_path}") - return None - - try: - from PIL import Image - img = Image.open(png_path) - # Order from largest to smallest - Windows uses first size as primary display - sizes = [(256, 256), (128, 128), (64, 64), (48, 48), (32, 32), (16, 16)] - icons = [] - - for size in sizes: - resized = img.resize(size, Image.Resampling.LANCZOS) - if resized.mode != 'RGBA': - resized = resized.convert('RGBA') - icons.append(resized) - - # Save with largest icon as primary - icons[0].save(ico_path, format='ICO', append_images=icons[1:]) - print_success(f"Created {ico_path.name}") - return ico_path - - except Exception as e: - print_error(f"Failed to convert icon: {e}") - return None - -def get_msvc_env(): - """Get environment variables from vcvarsall.bat for VS 2026 Community. - - Manually invoke vcvarsall.bat and capture the resulting environment so - Nuitka/Scons can reliably locate cl.exe even when auto-detection misses it. - """ - vcvarsall = r"C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvarsall.bat" - - if not Path(vcvarsall).exists(): - print_info("vcvarsall.bat not found, falling back to default environment") - return None - - print_info(f"Setting up MSVC environment from VS 2026 Community...") - - try: - # Run vcvarsall.bat and capture the resulting environment - result = subprocess.run( - f'"{vcvarsall}" x64 && set', - capture_output=True, text=True, shell=True - ) - - if result.returncode != 0: - print_error(f"vcvarsall.bat failed: {result.stderr}") - return None - - # Parse environment variables from output - env = {} - for line in result.stdout.splitlines(): - if '=' in line: - key, _, value = line.partition('=') - env[key] = value - - print_success("MSVC environment configured") - return env - - except Exception as e: - print_error(f"Failed to set up MSVC environment: {e}") - return None - -def run_nuitka(icon_path): - print_step(3, "Compiling to native binary with Nuitka") - - # Create dist directory - DIST_DIR.mkdir(exist_ok=True) - - # Get MSVC environment (needed for VS 2026 Insiders which Nuitka can't auto-detect) - msvc_env = get_msvc_env() - - cmd = [ - sys.executable, "-m", "nuitka", - - # Output settings - "--standalone", # Create standalone distribution - "--output-dir=" + str(DIST_DIR), # Output directory - "--output-filename=" + APP_EXE_NAME, # Output executable name - - # Windows-specific - "--windows-console-mode=disable", # Hide console window (GUI app) - "--windows-company-name=" + APP_PUBLISHER, - "--windows-product-name=" + APP_NAME, - "--windows-file-version=" + APP_VERSION, - "--windows-product-version=" + APP_VERSION, - - # MSVC compiler (required for Python 3.13 - MinGW64 not supported) - # Environment from vcvarsall.bat is passed to subprocess so Nuitka finds cl.exe - "--msvc=latest", - - # Auto-accept download prompts (Dependency Walker, etc.) - "--assume-yes-for-downloads", - - # Code protection / optimization - "--lto=yes", # Link Time Optimization - "--jobs=2", # Reduced from 4 to prevent MSVC OOM - - # Allow importing modules excluded via --nofollow-import-to at runtime. - # We copy them as regular Python files in the post-build step. - "--no-deployment-flag=excluded-module-usage", - ] - - # Add icon if available - if icon_path and icon_path.exists(): - cmd.append(f"--windows-icon-from-ico={icon_path}") - - # NOTE: frontend/ and Auto_Use/windows_use/sandbox/terminal/ are now embedded in _embedded_resources.py - # and served via Flask routes - no need to include as data directories - # This completely hides all folder structure from the user - print_info("UI files embedded in binary (frontend/, terminal/)") - - # Create empty scratchpad folder structure post-build (done by app.py at runtime) - - # ========================================================================= - # COMPILE TO NATIVE BINARY - # Everything here is compiled by Nuitka to C → native binary. - # This was the working config before google-genai was added. - # ========================================================================= - include_packages = [ - "pywinauto", - "pywinauto.controls", - "pywinauto.controls.uiawrapper", - "PIL", - "numpy", - "requests", - "dotenv", - "keyboard", - "flask", - # Proprietary code (Auto_Use.windows_use) - "Auto_Use", - "Auto_Use.windows_use", - "Auto_Use.windows_use.llm_provider.openrouter.view", - "Auto_Use.windows_use.llm_provider.groq.view", - "Auto_Use.windows_use.llm_provider.openai.view", - "Auto_Use.windows_use.llm_provider.anthropic.view", - "Auto_Use.windows_use.llm_provider.google.view", - ] - - for package in include_packages: - cmd.append(f"--include-package={package}") - - include_modules = [ - "_embedded_resources", - "interception", - "comtypes", - "comtypes.client", - "comtypes.stream", - "win32api", - "win32con", - "win32gui", - "pydoc", # needed by scipy._lib._docscrape at runtime - ] - - for module in include_modules: - cmd.append(f"--include-module={module}") - - # ========================================================================= - # EXCLUDE FROM C COMPILATION (copied as Python by post-build step) - # - # Only packages that cause MSVC OOM or are open-source LLM SDKs. - # When adding new large packages in the future (e.g. Azure SDK), - # just add them here -- the post-build step copies them automatically. - # ========================================================================= - nofollow_third_party = [ - # Entire google namespace (genai, cloud, auth, api_core, protobuf) - # Must exclude as one unit to avoid namespace package conflicts - "google", - "aiohttp", - "grpc", - "protobuf", - "scipy", - "networkx", - - # LLM provider SDKs (open-source, no need to compile) - "openai", - "anthropic", - "groq", - - # Never needed - "tkinter", - "matplotlib", - "pytest", - "setuptools", - ] - - for pkg in nofollow_third_party: - cmd.append(f"--nofollow-import-to={pkg}") - - # NOTE: Auto_Use is NOT copied as data - all resources are embedded in binary - # This completely hides folder structure (no agent/, controller/, tree/ visible) - - # Add entry point - cmd.append(str(ROOT_DIR / ENTRY_POINT)) - - print_info("Running Nuitka (this may take 5-15 minutes on first build)...") - print_info("Command: " + " ".join(cmd[:10]) + " ...") - - try: - # Run Nuitka with real-time output, using MSVC environment if available - process = subprocess.Popen( - cmd, - cwd=ROOT_DIR, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - bufsize=1, - env=msvc_env # Pass MSVC environment so Nuitka/Scons can find cl.exe - ) - - # Stream all output in real-time (no filtering, so we can see errors/prompts) - for line in process.stdout: - line = line.rstrip() - if line: - print(f" {line}") - - process.wait() - - if process.returncode != 0: - print_error(f"Nuitka failed with return code {process.returncode}") - return False - - print_success("Nuitka compilation completed successfully") - return True - - except FileNotFoundError: - print_error("Nuitka not found! Install with: pip install nuitka ordered-set zstandard") - return False - except Exception as e: - print_error(f"Nuitka failed: {e}") - return False - -def organize_dist(): - """Organize Nuitka output to match expected structure for Inno Setup""" - print_step(4, "Organizing distribution files") - - # Nuitka creates: dist/app.dist/ containing the executable and dependencies - nuitka_dist = DIST_DIR / f"{ENTRY_POINT.replace('.py', '.dist')}" - final_dist = DIST_DIR / APP_EXE_NAME - - if not nuitka_dist.exists(): - # Check alternative location - nuitka_dist = ROOT_DIR / f"{ENTRY_POINT.replace('.py', '.dist')}" - - if not nuitka_dist.exists(): - print_error(f"Nuitka output not found at expected locations") - # List what's in dist - if DIST_DIR.exists(): - print_info(f"Contents of {DIST_DIR}:") - for item in DIST_DIR.iterdir(): - print_info(f" - {item.name}") - return False - - # Rename/move to expected location - if final_dist.exists(): - shutil.rmtree(final_dist) - - shutil.move(str(nuitka_dist), str(final_dist)) - - # Rename the executable if needed - old_exe = final_dist / f"{ENTRY_POINT.replace('.py', '.exe')}" - new_exe = final_dist / f"{APP_EXE_NAME}.exe" - - if old_exe.exists() and old_exe != new_exe: - old_exe.rename(new_exe) - - print_success(f"Distribution organized at {final_dist}") - - # List final contents - exe_count = len(list(final_dist.glob("*.exe"))) - dll_count = len(list(final_dist.glob("*.dll"))) - pyd_count = len(list(final_dist.glob("**/*.pyd"))) - print_info(f"Contents: {exe_count} exe, {dll_count} dll, {pyd_count} pyd files") - - return True - -def create_self_signed_cert(): - """Create a self-signed certificate for code signing if none exists""" - cert_path = ROOT_DIR / "certs" - pfx_path = cert_path / "AutoUse.pfx" - - if pfx_path.exists(): - print_info("Using existing certificate") - return pfx_path, "AutoUse123" # Default password for self-signed - - print_info("Creating self-signed certificate...") - cert_path.mkdir(exist_ok=True) - - # PowerShell command to create self-signed certificate - ps_script = f''' -$cert = New-SelfSignedCertificate ` - -Type CodeSigningCert ` - -Subject "CN={APP_PUBLISHER}, O={APP_NAME}" ` - -KeyAlgorithm RSA ` - -KeyLength 2048 ` - -HashAlgorithm SHA256 ` - -CertStoreLocation Cert:\\CurrentUser\\My ` - -NotAfter (Get-Date).AddYears(5) - -$pwd = ConvertTo-SecureString -String "AutoUse123" -Force -AsPlainText -Export-PfxCertificate -Cert $cert -FilePath "{pfx_path}" -Password $pwd - -# Also export to trusted root for local testing (optional) -$rootStore = New-Object System.Security.Cryptography.X509Certificates.X509Store("Root", "CurrentUser") -$rootStore.Open("ReadWrite") -$rootStore.Add($cert) -$rootStore.Close() - -Write-Host "Certificate created and exported to {pfx_path}" -Write-Host "Thumbprint: $($cert.Thumbprint)" -''' - - try: - result = subprocess.run( - ["powershell", "-ExecutionPolicy", "Bypass", "-Command", ps_script], - capture_output=True, - text=True - ) - - if result.returncode == 0 and pfx_path.exists(): - print_success(f"Self-signed certificate created: {pfx_path}") - return pfx_path, "AutoUse123" - else: - print_error(f"Failed to create certificate: {result.stderr}") - return None, None - - except Exception as e: - print_error(f"Certificate creation failed: {e}") - return None, None - -def sign_executable(exe_path, cert_path=None, password=None): - """Sign an executable with a code signing certificate""" - - if not exe_path.exists(): - print_error(f"Executable not found: {exe_path}") - return False - - # Determine certificate to use - if cert_path and Path(cert_path).exists(): - pfx_path = Path(cert_path) - pfx_password = password - elif CODE_SIGN_CERT_PATH and Path(CODE_SIGN_CERT_PATH).exists(): - pfx_path = Path(CODE_SIGN_CERT_PATH) - pfx_password = CODE_SIGN_PASSWORD - else: - # Create self-signed certificate - pfx_path, pfx_password = create_self_signed_cert() - if not pfx_path: - print_error("No certificate available for signing") - return False - - # Find signtool.exe - signtool_paths = [ - r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe", - r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64\signtool.exe", - r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe", - r"C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe", - ] - - signtool = None - for path in signtool_paths: - if Path(path).exists(): - signtool = path - break - - if not signtool: - # Try to find it via where command - try: - result = subprocess.run(["where", "signtool"], capture_output=True, text=True) - if result.returncode == 0: - signtool = result.stdout.strip().split('\n')[0] - except: - pass - - if not signtool: - print_error("signtool.exe not found!") - print_info("Install Windows SDK or Visual Studio with Windows SDK component") - print_info("Download: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/") - return False - - # Sign the executable - cmd = [ - signtool, - "sign", - "/f", str(pfx_path), - "/p", pfx_password, - "/fd", "SHA256", - "/tr", CODE_SIGN_TIMESTAMP_URL, - "/td", "SHA256", - "/d", APP_NAME, - str(exe_path) - ] - - try: - result = subprocess.run(cmd, capture_output=True, text=True) - - if result.returncode == 0: - print_success(f"Signed: {exe_path.name}") - return True - else: - print_error(f"Signing failed: {result.stderr}") - return False - - except Exception as e: - print_error(f"Signing error: {e}") - return False - -def sign_all_executables(): - """Sign all executables in the distribution""" - print_step("4b", "Code signing executables") - - dist_path = DIST_DIR / APP_EXE_NAME - - if not dist_path.exists(): - print_error("Distribution directory not found") - return False - - # Sign main executable - main_exe = dist_path / f"{APP_EXE_NAME}.exe" - if main_exe.exists(): - if not sign_executable(main_exe): - print_info("Continuing without signature (UAC will show 'Unknown Publisher')") - - # Optionally sign DLLs (usually not necessary) - # for dll in dist_path.glob("*.dll"): - # sign_executable(dll) - - return True - -def create_inno_setup_script(): - print_step(5, "Creating Inno Setup script") - - dist_path = str(DIST_DIR / APP_EXE_NAME).replace("\\", "\\\\") - output_path = str(OUTPUT_DIR).replace("\\", "\\\\") - ico_path = str(ROOT_DIR / "auto_use.ico").replace("\\", "\\\\") - license_path = str(ROOT_DIR / "LICENSE").replace("\\", "\\\\") - interception_installer = str(ROOT_DIR / "Interception" / "command line installer" / "install-interception.exe").replace("\\", "\\\\") - - iss_content = f'''; Auto Use - Inno Setup Script (Nuitka Build) - -#define MyAppName "{APP_NAME}" -#define MyAppVersion "{APP_VERSION}" -#define MyAppPublisher "{APP_PUBLISHER}" -#define MyAppExeName "{APP_EXE_NAME}.exe" - -[Setup] -AppId={{{{8F3E9A5B-2C1D-4E7F-A8B9-1234567890AB}}}} -AppName={{#MyAppName}} -AppVersion={{#MyAppVersion}} -AppPublisher={{#MyAppPublisher}} -DefaultDirName={{localappdata}}\\{{#MyAppName}} -DefaultGroupName={{#MyAppName}} -AllowNoIcons=yes -OutputDir={output_path} -OutputBaseFilename=AutoUse_Setup -SetupIconFile={ico_path} -UninstallDisplayIcon={{app}}\\{{#MyAppExeName}} -Compression=lzma2/max -SolidCompression=yes -PrivilegesRequired=admin -MinVersion=10.0 -WizardStyle=modern -LicenseFile={license_path} -AlwaysRestart=yes - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl" - -[Tasks] -Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional shortcuts:"; Flags: checkedonce - -[Files] -Source: "{dist_path}\\*"; DestDir: "{{app}}"; Flags: ignoreversion recursesubdirs createallsubdirs -Source: "{interception_installer}"; DestDir: "{{app}}\\InterceptionDriver"; Flags: ignoreversion - -[Icons] -Name: "{{group}}\\{{#MyAppName}}"; Filename: "{{app}}\\{{#MyAppExeName}}"; WorkingDir: "{{app}}" -Name: "{{group}}\\Uninstall {{#MyAppName}}"; Filename: "{{uninstallexe}}" -Name: "{{commondesktop}}\\{{#MyAppName}}"; Filename: "{{app}}\\{{#MyAppExeName}}"; WorkingDir: "{{app}}"; Tasks: desktopicon - -[Run] -Filename: "{{app}}\\InterceptionDriver\\install-interception.exe"; Parameters: "/install"; Flags: runhidden waituntilterminated; StatusMsg: "Installing Interception driver..." - -[UninstallRun] -Filename: "{{app}}\\InterceptionDriver\\install-interception.exe"; Parameters: "/uninstall"; Flags: runhidden waituntilterminated; RunOnceId: "InterceptionUninstall" - -[UninstallDelete] -Type: filesandordirs; Name: "{{app}}" - -[Messages] -FinishedLabel=Setup has finished installing [name] on your computer.%n%nIMPORTANT: A RESTART IS REQUIRED for the Auto Use driver to work properly.%n%nClick Finish to restart your computer now. - -[Code] -procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); -var - ResultCode: Integer; -begin - if CurUninstallStep = usPostUninstall then - begin - DeleteFile(ExpandConstant('{{sys}}\\drivers\\keyboard.sys')); - DeleteFile(ExpandConstant('{{sys}}\\drivers\\mouse.sys')); - - if MsgBox('Uninstallation complete.' + #13#10 + #13#10 + - 'A restart is REQUIRED to fully remove the Auto Use driver.' + #13#10 + #13#10 + - 'Restart now?', - mbConfirmation, MB_YESNO) = IDYES then - begin - Exec('shutdown.exe', '/r /t 5 /c "Restarting to complete Auto Use uninstallation..."', '', SW_HIDE, ewNoWait, ResultCode); - end; - end; -end; - -function NeedRestart(): Boolean; -begin - Result := True; -end; -''' - - iss_path = ROOT_DIR / "installer.iss" - with open(iss_path, 'w', encoding='utf-8') as f: - f.write(iss_content) - - print_success(f"Created {iss_path.name}") - return iss_path - -def run_inno_setup(iss_path): - print_step(6, "Compiling installer with Inno Setup") - - if not Path(INNO_SETUP_COMPILER).exists(): - print_error(f"Inno Setup not found at: {INNO_SETUP_COMPILER}") - return False - - OUTPUT_DIR.mkdir(exist_ok=True) - - try: - result = subprocess.run( - [INNO_SETUP_COMPILER, str(iss_path)], - check=True, - capture_output=True, - text=True - ) - print_success("Inno Setup completed successfully") - return True - except subprocess.CalledProcessError as e: - print_error(f"Inno Setup failed!") - if e.stdout: - print_error(f"STDOUT: {e.stdout}") - if e.stderr: - print_error(f"STDERR: {e.stderr}") - return False - -def move_final_installer(): - print_step(7, "Finalizing") - - installer_path = OUTPUT_DIR / "AutoUse_Setup.exe" - final_path = ROOT_DIR / "AutoUse_Setup.exe" - - if installer_path.exists(): - if final_path.exists(): - final_path.unlink() - shutil.move(str(installer_path), str(final_path)) - size_mb = final_path.stat().st_size / (1024 * 1024) - print_success(f"Installer ready: {final_path.name} ({size_mb:.1f} MB)") - return final_path - else: - print_error("Installer not found") - return None - -def cleanup(): - """Clean up build artifacts""" - for dir_path in [BUILD_DIR, OUTPUT_DIR]: - if dir_path.exists(): - shutil.rmtree(dir_path) - - # Clean Nuitka build artifacts (keep dist for debugging if needed) - nuitka_build = ROOT_DIR / f"{ENTRY_POINT.replace('.py', '.build')}" - nuitka_onefile = ROOT_DIR / f"{ENTRY_POINT.replace('.py', '.onefile-build')}" - - for nuitka_dir in [nuitka_build, nuitka_onefile]: - if nuitka_dir.exists(): - shutil.rmtree(nuitka_dir) - - # Remove installer script - iss_file = ROOT_DIR / "installer.iss" - if iss_file.exists(): - iss_file.unlink() - - print_success("Cleanup complete") - -# ============================================================================= -# MAIN -# ============================================================================= - -def main(): - print("\n" + "="*60) - print(" AUTO USE - PRODUCTION BUILD (NUITKA)") - print(" Native Binary Compilation for Code Protection") - print("="*60) - - # Step 0: Pre-flight checks + pack embedded resources - pre_flight_checks() - if not pack_embedded_resources(): - print_error("Resource packing failed!") - sys.exit(1) - - # Step 1: Clean - clean_build_directories() - - # Step 2: Icon - icon_path = convert_png_to_ico() - - # Step 3: Nuitka compilation - if not run_nuitka(icon_path): - print_error("Nuitka compilation failed!") - sys.exit(1) - - # Step 4: Organize distribution - if not organize_dist(): - print_error("Failed to organize distribution!") - sys.exit(1) - - # Step 4a: Copy third-party packages into dist (excluded from Nuitka compilation) - if not copy_third_party_packages(): - print_error("Failed to copy third-party packages!") - sys.exit(1) - - # Step 4b: Code sign executables - sign_all_executables() - - # Step 5: Create Inno Setup script - iss_path = create_inno_setup_script() - - # Step 6: Run Inno Setup - if not run_inno_setup(iss_path): - print_error("Inno Setup failed!") - sys.exit(1) - - # Step 7: Move final installer - final_installer = move_final_installer() - - # Step 7b: Sign the installer - if final_installer: - print_step("7b", "Signing installer") - sign_executable(final_installer) - - if final_installer: - cleanup() - print("\n" + "="*60) - print(" [OK] BUILD SUCCESSFUL!") - print(f" Output: {final_installer.name}") - print(" ") - print(" Code Protection: Native binary (C compiled)") - print(" Reverse Engineering: Significantly harder") - print(f" Publisher: {APP_PUBLISHER}") - print(" ") - print(" NOTE: Self-signed cert shows your name in UAC but") - print(" still shows a warning. For full trust with no warning,") - print(" purchase a code signing certificate (~$200-500/year).") - print("="*60 + "\n") - else: - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file From ad168e16086820e40a88a7ee7efbd91e10c0ce9b Mon Sep 17 00:00:00 2001 From: FunctionFreak Date: Thu, 18 Jun 2026 22:43:09 +0100 Subject: [PATCH 5/5] mac os scrolling fixed --- Auto_Use/macOS_use/controller/service.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Auto_Use/macOS_use/controller/service.py b/Auto_Use/macOS_use/controller/service.py index 18d174a..7b24e81 100644 --- a/Auto_Use/macOS_use/controller/service.py +++ b/Auto_Use/macOS_use/controller/service.py @@ -592,23 +592,29 @@ def scroll(self, index, direction): # Warp cursor to scroll target self._warp_cursor_only(center_x, center_y) + # macOS scrolls in LINE units — pyautogui posts one scroll event per unit, + # so this is a small, gentle nudge. This is NOT the Windows "120 == one wheel + # notch" convention: a value like 120 here becomes ~120 line events and macOS + # momentum scrolling then flings the view straight to the end. Keep it small. + # ~3 steps x 5 lines = a modest scroll; tune scroll_clicks to taste. scroll_amount = 3 + scroll_clicks = 5 if direction.lower() == "up": for _ in range(scroll_amount): - pyautogui.scroll(120, x=center_x, y=center_y) + pyautogui.scroll(scroll_clicks, x=center_x, y=center_y) time.sleep(0.05) elif direction.lower() == "down": for _ in range(scroll_amount): - pyautogui.scroll(-120, x=center_x, y=center_y) + pyautogui.scroll(-scroll_clicks, x=center_x, y=center_y) time.sleep(0.05) elif direction.lower() == "left": for _ in range(scroll_amount): - pyautogui.hscroll(-120, x=center_x, y=center_y) + pyautogui.hscroll(-scroll_clicks, x=center_x, y=center_y) time.sleep(0.05) elif direction.lower() == "right": for _ in range(scroll_amount): - pyautogui.hscroll(120, x=center_x, y=center_y) + pyautogui.hscroll(scroll_clicks, x=center_x, y=center_y) time.sleep(0.05) else: return {"status": "error", "action": "scroll", "index": index,