"""Union executor: model JSON -> gated, whitelisted OS command. plan(cmd, platform) -> Plan (kind, command, note, risk, needs_sudo, supported) gate(plan, allow_caution, confirm_text) -> (ok, reason) run(plan, dry_run=True) -> {"status": ok|error|timeout|dry_run|skipped, "output": ..., "command": ...} The model only produces text; nothing runs unless the catalog knows the action and the gate passes. """ import json, os, re, shlex, subprocess, sys, time, threading, urllib.parse, webbrowser HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE) from commands import CMD # noqa import tables as T # noqa CATALOG = json.load(open(os.path.join(HERE, "..", "model", "catalog.json")))["actions"] # ---------------------------------------------------------------- static tables BCM_TO_BOARD = {2: 3, 3: 5, 4: 7, 14: 8, 15: 10, 17: 11, 18: 12, 27: 13, 22: 15, 23: 16, 24: 18, 10: 19, 9: 21, 25: 22, 11: 23, 8: 24, 7: 26, 0: 27, 1: 28, 5: 29, 6: 31, 12: 32, 13: 33, 19: 35, 16: 36, 26: 37, 20: 38, 21: 40} BOARD_TO_BCM = {v: k for k, v in BCM_TO_BOARD.items()} BOARD_POWER = {1: "3.3V", 17: "3.3V", 2: "5V", 4: "5V", 6: "GND", 9: "GND", 14: "GND", 20: "GND", 25: "GND", 30: "GND", 34: "GND", 39: "GND"} BCM_ROLE = {2: "I2C1 SDA", 3: "I2C1 SCL", 14: "UART TXD", 15: "UART RXD", 18: "PCM CLK / PWM0", 19: "PCM FS / PWM1 / SPI1 MISO", 12: "PWM0", 13: "PWM1", 10: "SPI0 MOSI", 9: "SPI0 MISO", 11: "SPI0 SCLK", 8: "SPI0 CE0", 7: "SPI0 CE1", 0: "ID_SD (EEPROM)", 1: "ID_SC (EEPROM)", 4: "GPCLK0 / 1-Wire default", 20: "PCM DIN / SPI1 MOSI", 21: "PCM DOUT / SPI1 SCLK", 16: "SPI1 CE2", 17: "SPI1 CE1"} ROLE_TO_PIN = {"sda": "GPIO2 (board 3)", "scl": "GPIO3 (board 5)", "txd": "GPIO14 (board 8)", "rxd": "GPIO15 (board 10)", "ground": "board 6,9,14,20,25,30,34,39", "gnd": "board 6,9,14,20,25,30,34,39", "3.3v": "board 1,17", "3v3": "board 1,17", "5v": "board 2,4", "pwm": "GPIO12/13/18/19", "mosi": "GPIO10 (board 19)", "miso": "GPIO9 (board 21)", "sclk": "GPIO11 (board 23)", "ce0": "GPIO8 (board 24)", "i2c": "GPIO2 SDA / GPIO3 SCL (board 3/5)", "spi": "GPIO10/9/11/8/7 (board 19/21/23/24/26)", "uart": "GPIO14 TXD / GPIO15 RXD (board 8/10)", "pcm": "GPIO18/19/20/21 (board 12/35/38/40)"} POWER_GUID = {"performance": "8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c", "balanced": "381b4222-f694-41f0-9685-ff5bb260df2e", "power_saver": "a1841308-3541-4fab-bc81-f71556f20b4a"} GOV_FOR_MODE = {"performance": "performance", "balanced": "ondemand", "power_saver": "powersave"} TZ_WIN = {"asia/kolkata": "India Standard Time", "utc": "UTC", "asia/dubai": "Arabian Standard Time", "america/new_york": "Eastern Standard Time", "europe/london": "GMT Standard Time", "asia/tokyo": "Tokyo Standard Time", "asia/singapore": "Singapore Standard Time", "australia/sydney": "AUS Eastern Standard Time"} VK = dict(T.VK, **{"escape": 0x1B, "esc": 0x1B, "enter": 0x0D, "return": 0x0D, "space": 0x20, "backspace": 0x08, "delete": 0x2E, "del": 0x2E, "home": 0x24, "end": 0x23, "pageup": 0x21, "pagedown": 0x22, "page_down": 0x22, "page_up": 0x21, "period": 0xBE, "n": 0x4E, "r": 0x52, "p": 0x50, "h": 0x48, "printscreen": 0x2C, "f11": 0x7A, "f1": 0x70, "f2": 0x71, "f3": 0x72, "f6": 0x75, "f10": 0x79, "f12": 0x7B}) XKEY = {"escape": "Escape", "esc": "Escape", "enter": "Return", "return": "Return", "space": "space", "backspace": "BackSpace", "delete": "Delete", "del": "Delete", "home": "Home", "end": "End", "pageup": "Page_Up", "pagedown": "Page_Down", "page_down": "Page_Down", "page_up": "Page_Up", "tab": "Tab", "up": "Up", "down": "Down", "left": "Left", "right": "Right", "f5": "F5", "f11": "F11", "f4": "F4", "period": "period", "plus": "plus", "minus": "minus"} CRON = [("every 5 minutes", "*/5 * * * *"), ("every hour", "0 * * * *"), ("har ghante", "0 * * * *"), ("daily", "0 6 * * *"), ("roz subah", "0 6 * * *"), ("at 3 am", "0 3 * * *"), ("on reboot", "@reboot"), ("every monday", "0 9 * * 1"), ("at midnight", "0 0 * * *")] def platform_name(): return "win" if os.name == "nt" else "pi" def is_root(): return hasattr(os, "geteuid") and os.geteuid() == 0 class Plan: def __init__(self, action, kind, command, note="", supported=True, risk="safe", needs_sudo=False, platform="pi", meta=None): self.action, self.kind, self.command, self.note = action, kind, command, note self.supported, self.risk, self.needs_sudo, self.platform = supported, risk, needs_sudo, platform self.meta = meta or {} def to_dict(self): return {"action": self.action, "kind": self.kind, "command": self.command, "note": self.note, "supported": self.supported, "risk": self.risk, "needs_sudo": self.needs_sudo, "platform": self.platform, **self.meta} # ---------------------------------------------------------------- quoting def q_sh(v): return shlex.quote(str(v)) def q_ps(v): return "'" + str(v).replace("'", "''") + "'" def q_cmd(v): s = str(v).replace('"', "") return f'"{s}"' if re.search(r"[\s&|<>^]", s) else s QUOTE = {"sh": q_sh, "ps": q_ps, "cmd": q_cmd} def derived(cmd, plat, action): """Fill the derived placeholders. Raw slot values are quoted per shell by render().""" d = {} a, u = cmd.get("amount"), cmd.get("unit") secs = (a or 0) * {"sec": 1, "min": 60, "hour": 3600}.get(u, 60) if a is not None else 0 d["seconds"] = secs; d["minutes"] = max(0, round(secs / 60)) if secs else 0 d["step"] = cmd.get("step", 10) spec = CATALOG[action] d["sudo"] = "sudo " if (spec.get("needs_sudo") and plat == "pi" and not is_root()) else "" d["home"] = os.path.expanduser("~") if "app" in cmd: ap = T.APPS.get(cmd["app"], {}); d["app_win"] = ap.get("win") or ""; d["app_pi"] = ap.get("pi") or "" d["proc_win"] = T.WIN_PROC.get(cmd["app"]) or cmd["app"]; d["proc_pi"] = T.PI_PROC.get(cmd["app"]) or cmd["app"] if "site" in cmd and action != "web_login": d["site_url"] = T.SITES.get(cmd["site"], "https://" + cmd["site"]) if "url" in cmd: d["url_full"] = cmd["url"] if cmd["url"].startswith(("http://", "https://")) else "https://" + cmd["url"] for k in ("query", "subject", "text"): if k in cmd: d[k + "_url"] = urllib.parse.quote(str(cmd[k])) if "folder" in cmd: f = T.FOLDERS.get(cmd["folder"], {}); d["folder_win"] = f.get("win", "%USERPROFILE%"); d["folder_pi"] = f.get("pi", "~") d["page_uri"] = T.SETTINGS_PAGES.get(cmd.get("page", ""), "ms-settings:") if "service" in cmd: d["svc_unit"] = T.SERVICE_UNITS.get(cmd["service"], cmd["service"]); d["svc_win"] = T.WIN_SERVICES.get(cmd["service"], cmd["service"]) if "mode" in cmd: d["mode_cmd"] = T.DISPLAY_MODES.get(cmd["mode"], "DisplaySwitch.exe /extend"); d["power_guid"] = POWER_GUID.get(cmd["mode"], POWER_GUID["balanced"]) d["governor_for_mode"] = GOV_FOR_MODE.get(cmd["mode"], "ondemand"); d["mode_pc"] = {"input": "ip", "output": "op"}.get(cmd["mode"], "ip") if "pull" in cmd: d["pull_pc"] = {"up": "pu", "down": "pd", "none": "pn"}.get(cmd["pull"], "pn") if "edge" in cmd: d["edge_g"] = {"rising": "rising", "falling": "falling", "both": "both"}.get(cmd["edge"], "both") else: d["edge_g"] = "both" if "pin" in cmd: pin = int(cmd["pin"]) if cmd.get("numbering") == "board": pin = BOARD_TO_BCM.get(pin, -1) d["gpio"] = pin if "interface" in cmd: d["iface_rc"] = {"i2c": "i2c", "spi": "spi", "serial": "serial_hw", "onewire": "onewire", "camera": "legacy"}.get(cmd["interface"], cmd["interface"]) if "orientation" in cmd: d["orientation_wl"] = {"landscape": "normal", "portrait": "90", "landscape_flipped": "180"}.get(cmd["orientation"], "normal") if "key" in cmd: d["key_x"] = XKEY.get(cmd["key"], cmd["key"]) if "text" in cmd: d["text_sendkeys"] = q_ps(re.sub(r"([+^%~(){}\[\]])", r"{\1}", str(cmd["text"]))) if "tz" in cmd: d["tz_win"] = TZ_WIN.get(str(cmd["tz"]).lower(), "UTC") if "target" in cmd: d["boot_target"] = "graphical" if cmd["target"] in ("desktop", "graphical") else "multi-user" if "priority" in cmd: p = str(cmd["priority"]); d["priority_num"] = {"low": 10, "high": -5}.get(p, p if p.lstrip("-").isdigit() else 5) if "schedule" in cmd: s = str(cmd["schedule"]).lower(); d["cron_expr"] = next((c for k, c in CRON if k in s), "0 * * * *") if "format" in cmd: f = str(cmd["format"]).lower(); d["format_ext"] = "wav" if "wav" in f else "txt" if f == "text" else f.split()[-1] if "freq" not in cmd: d["freq"] = 1000 if "interval" not in cmd: d["interval"] = 500 if action == "gpio_blink" else 2 if "count" not in cmd: d["count"] = 5 if "user" not in cmd: d["user"] = "pi" if "baud" not in cmd: d["baud"] = 115200 if "password" not in cmd: d["password"] = "" d["pw_arg"] = (" password " + q_sh(cmd["password"])) if cmd.get("password") else "" if action == "hotspot_on" and "ssid" not in cmd: d["ssid"] = "PiHotspot" return d def render(action, cmd, plat): spec = CATALOG.get(action) if not spec: return Plan(action, "py", "", "unknown action", supported=False) tmpl = CMD.get(action, {}).get(plat) risk, sudo = spec["risk"], spec.get("needs_sudo", False) plat_name = "windows" if plat == "win" else "pi" if plat_name not in spec["platforms"] or not tmpl: return Plan(action, "none", "", f"'{action}' is not available on {'Windows' if plat == 'win' else 'Raspberry Pi'}", supported=False, risk=risk, needs_sudo=sudo, platform=plat) kind, _, body = tmpl.partition(":") quote = QUOTE.get(kind, lambda v: str(v)) vals = {} RAW = {"run_command", "run_script", "run_background", "watch_command", "schedule_task", "create_service"} for k, v in cmd.items(): if k == "action": continue raw = isinstance(v, int) or kind in ("open", "key", "py") or (k == "command" and action in RAW) vals[k] = v if raw else quote(v) d = derived(cmd, plat, action) for k, v in d.items(): if k not in vals: vals[k] = quote(v) if k == "password" else v if "pin" in cmd and kind != "py": g = d.get("gpio", -1) if cmd.get("numbering") == "board" and int(cmd["pin"]) in BOARD_POWER: return Plan(action, kind, "", f"physical pin {cmd['pin']} is a {BOARD_POWER[int(cmd['pin'])]} pin, not a GPIO - refused", supported=False, risk=risk, needs_sudo=sudo, platform=plat) if not (isinstance(g, int) and 0 <= g <= 27): return Plan(action, kind, "", f"pin {cmd['pin']} ({cmd.get('numbering', 'bcm')}) is not a usable GPIO (BCM 0-27) - refused", supported=False, risk=risk, needs_sudo=sudo, platform=plat) try: command = body.format(**vals) except KeyError as e: return Plan(action, kind, body, f"missing value for {e}", supported=False, risk=risk, needs_sudo=sudo, platform=plat) note = spec.get("desc", "") if kind == "sh" and d.get("sudo") and "sudo" not in command and sudo: command = "sudo " + command return Plan(action, kind, command, note, True, risk, sudo, plat, {"gpio": d.get("gpio")}) def plan(cmd, plat=None, text=None): """text = the user's original sentence; when given, GPIO writes that name a bare 'pin N' ask board vs BCM.""" plat = plat or platform_name() a = cmd.get("action", "unknown") if a == "clarify": return Plan("clarify", "py", "", f"need more information for '{cmd.get('action_ref', cmd.get('target', ''))}'".strip(), True, "safe", False, plat) bad = suspicious_credentials(cmd) or ambiguous_pin(cmd, text) if bad: return Plan("clarify", "py", "", bad, True, "safe", False, plat, {"clarify_for": a}) return render(a, cmd, plat) # 'pin no 11' is labelled board in some training rows and BCM in others, so the model's numbering for a bare pin is a # guess. For anything that drives a pin (risk above safe) the user must say which numbering they mean. _PIN_QUALIFIER = re.compile(r"\b(gpio|bcm|board|physical|phys|header)", re.I) def ambiguous_pin(cmd, text): """Return a clarification message when a pin-driving action came from text with no gpio/bcm/board qualifier, else ''.""" a = cmd.get("action"); spec = CATALOG.get(a, {}) if text is None or "pin" not in cmd or "pin" not in spec.get("slots", []) or spec.get("risk") == "safe": return "" if _PIN_QUALIFIER.search(text): return "" n = int(cmd["pin"]); as_board = BOARD_TO_BCM.get(n); as_bcm = BCM_TO_BOARD.get(n) board = f"physical/board pin {n}" + (f" (= GPIO {as_board})" if as_board is not None else " (not a GPIO)") bcm = f"GPIO/BCM {n}" + (f" (= board pin {as_bcm})" if as_bcm is not None else " (not on the header)") return f"pin {n} is ambiguous: {board} or {bcm}? Say 'board pin {n}' or 'gpio {n}'" # words that are never a real password/ssid on their own: a model slip that copied a filler instead of the value _FILLER = {"hai", "hain", "he", "h", "ka", "ki", "ke", "ko", "se", "pe", "par", "aur", "and", "is", "was", "to", "the", "please", "plz", "password", "pass", "pwd", "passwd", "key", "naam", "name", "ssid", "wifi", "wi-fi", "network", "uska", "iska", "mera", "mere", "ghar", "connect", "karo", "kar", "do", "jodo", "jod", "lagao", "yaar", "abhi", "jaldi", "pehle", "baad", "mein", "phir"} # words that should never be inside a network name: seeing one means the model's copy span slipped _NOT_IN_SSID = {"hai", "hain", "password", "pass", "pwd", "passwd", "key", "connect", "karo", "kar", "jodo", "ka", "ki", "ke", "se", "ghar", "mere", "naam", "pehle", "baad", "mein", "phir"} _CRED_ACTIONS = {"connect_wifi", "hotspot_on", "web_login", "forget_wifi", "show_wifi_password"} def suspicious_credentials(cmd): """Return a clarification message when the parsed credential slots look like a mis-copy, else ''.""" a = cmd.get("action") if a not in _CRED_ACTIONS: return "" name = str(cmd.get("ssid") or cmd.get("username") or "").strip() pw = cmd.get("password") if pw is not None: pws = str(pw).strip(); words = [w.strip(":,.;") for w in re.split(r"\s+", pws.lower())] if not pws or all(w in _FILLER for w in words): return f"password looks wrong ('{pws}'), please say the password again" if len(words) > 1 and any(w in _FILLER for w in words): return f"password '{pws}' looks like part of the sentence, please say the password again" if name and pws.lower() == name.lower(): return "password came out the same as the network/user name, please repeat both" if "ssid" in cmd and name: words = [w.strip(":,.;") for w in re.split(r"\s+", name.lower())] if all(w in _FILLER for w in words) or any(w in _NOT_IN_SSID for w in words): return f"network name '{name}' looks wrong, please give the network name (and password) again" return "" # ---------------------------------------------------------------- gating def gate(p, allow_caution=False, confirm_text=None): if not p.supported: return False, p.note if p.risk == "critical": return (confirm_text == p.action), f"critical action: type the action name '{p.action}' to confirm" if p.risk == "caution" and not allow_caution: return False, "caution action: enable 'allow caution actions' or confirm" return True, "ok" # ---------------------------------------------------------------- execution _timers = {} _last = {"plan": None} def _open(target): try: if os.name == "nt": os.startfile(target); return "opened " + target r = subprocess.run(["xdg-open", target], capture_output=True, text=True, timeout=10) return ("opened " if r.returncode == 0 else "could not open (no desktop session): ") + target except Exception as e: return f"open failed: {e}" def _key(combo, plat): keys = [k.strip().lower() for k in combo.split("+")] if plat == "win": vks = [VK.get(k) for k in keys] if None in vks: return "cmd_fail", f"unknown key in {combo}" ps = T.WIN_KEYS + f"[K]::Press([byte[]]@({','.join(hex(v) for v in vks)}))" return _shell("ps", ps) x = "+".join(XKEY.get(k, k) for k in keys).replace("win+", "super+") return _shell("sh", f"xdotool key {x} 2>/dev/null || wtype -k {x.split('+')[-1]}") def _powershell(): """PATH may not include PowerShell (e.g. started from Git Bash or a Store Python); fall back to the system copy.""" import shutil return shutil.which("powershell") or os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "WindowsPowerShell", "v1.0", "powershell.exe") def _shell(kind, command, timeout=25): try: if kind == "ps": exe = _powershell() if os.name == "nt" else "pwsh" import base64 enc = base64.b64encode(("$ProgressPreference='SilentlyContinue'; " + command).encode("utf-16-le")).decode() argv = [exe, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", enc] r = subprocess.run(argv, capture_output=True, text=True, timeout=timeout) elif kind == "cmd": if os.name != "nt": return "unsupported", "cmd.exe command - not runnable on Linux" r = subprocess.run(f'cmd.exe /d /s /c "{command}"', capture_output=True, text=True, timeout=timeout, shell=False) else: if os.name == "nt": return "unsupported", "bash command - not runnable on Windows" r = subprocess.run(["bash", "-lc", command], capture_output=True, text=True, timeout=timeout) out = (r.stdout + ("\n" + r.stderr if r.stderr.strip() else "")).strip() return ("ok" if r.returncode == 0 else "error"), out[-4000:] or f"(exit {r.returncode})" except subprocess.TimeoutExpired: return "timeout", f"no answer in {timeout}s" except FileNotFoundError as e: return "error", f"shell not found: {e}" def _builtin(name, cmd, plat): if name == "noop": return "ok", "not a device command - nothing done" if name == "clarify": return "ok", "asked user for the missing value" if name == "help": groups = {} for a, s in CATALOG.items(): groups.setdefault(s["risk"], []).append(a) return "ok", f"{len(CATALOG)} actions. safe: {len(groups.get('safe', []))}, caution: {len(groups.get('caution', []))}, critical: {len(groups.get('critical', []))}. Try: 'volume 60 karo', 'gpio 17 high karo', 'wifi Airtel_5G se connect karo password xyz'" if name in ("timer", "reminder"): secs = (cmd.get("amount", 0)) * {"sec": 1, "min": 60, "hour": 3600}.get(cmd.get("unit", "min"), 60) msg = cmd.get("text", "timer done") t = threading.Timer(secs, lambda: print(f"\a[{name}] {msg}")); t.daemon = True; t.start(); _timers[name] = t return "ok", f"{name} set for {secs}s" + (f": {msg}" if name == "reminder" else "") if name == "cancel_timer": t = _timers.pop("timer", None); return "ok", "timer cancelled" if t and not t.cancel() else "no active timer" if name == "alarm": return "ok", f"alarm registered for {cmd.get('time')} (in-process; use the OS clock app for persistence)" if name == "cancel_alarm": return "ok", "alarm cancelled" if name == "stopwatch_start": _timers["sw"] = time.time(); return "ok", "stopwatch started" if name == "stopwatch_stop": s = _timers.pop("sw", None); return "ok", f"stopwatch: {time.time() - s:.1f}s" if s else "stopwatch was not running" if name == "calculate": e = str(cmd.get("expression", "")).lower() e = re.sub(r"\bplus\b|\bjod\w*", "+", e); e = re.sub(r"\bminus\b", "-", e); e = re.sub(r"\btimes\b|\binto\b|\bx\b|\bguna\b", "*", e) e = re.sub(r"\bdivided by\b|\bbhaag\b", "/", e); e = re.sub(r"(\d+(?:\.\d+)?)\s*(?:ka|of)?\s*(\d+(?:\.\d+)?)\s*percent(?: of)?", r"(\1*\2/100)", e) e = re.sub(r"(\d+(?:\.\d+)?)\s*percent of\s*(\d+(?:\.\d+)?)", r"(\1*\2/100)", e); e = re.sub(r"sqrt of\s*(\d+)", r"(\1**0.5)", e) e = re.sub(r"(\d+)\s*(?:ki|to the)\s*power\s*(\d+)", r"(\1**\2)", e); e = re.sub(r"(\d+)\s*se\s*(\d+)\s*ghatao", r"(\1-\2)", e) if not re.fullmatch(r"[\d\s+\-*/().]+", e): return "error", f"could not parse expression: {cmd.get('expression')}" try: return "ok", f"{cmd.get('expression')} = {eval(e, {'__builtins__': {}}, {})}" except Exception as ex: return "error", str(ex) if name == "gpio_pin_role": if "role" in cmd: return "ok", f"{cmd['role']}: {ROLE_TO_PIN.get(str(cmd['role']).lower(), 'not a header role')}" pin = int(cmd.get("pin", -1)); board = cmd.get("numbering") == "board" if board and pin in BOARD_POWER: return "ok", f"board pin {pin} = {BOARD_POWER[pin]}" bcm = BOARD_TO_BCM.get(pin, -1) if board else pin return "ok", f"GPIO{bcm} (board {BCM_TO_BOARD.get(bcm, '?')}): {BCM_ROLE.get(bcm, 'general purpose I/O')}" if name == "bcm_to_board": p = int(cmd["pin"]); return "ok", f"GPIO{p} is physical pin {BCM_TO_BOARD.get(p, 'n/a (not on header)')}" if name == "board_to_bcm": p = int(cmd["pin"]) if p in BOARD_POWER: return "ok", f"board pin {p} is {BOARD_POWER[p]} (not a GPIO)" return "ok", f"physical pin {p} is GPIO{BOARD_TO_BCM.get(p, 'n/a')}" if name == "web_login": site = cmd.get("site", ""); url = site if site.startswith("http") else ("http://" + site if re.match(r"^(localhost|\d+\.\d+)", site) else "https://" + (site if "." in site else site + ".com")) msg = _open(url) return "ok", f"{msg}; login handler received username='{cmd.get('username')}' (password hidden) - form fill needs the browser-automation add-on" if name == "send_message": app, text = cmd.get("app", "sms"), urllib.parse.quote(str(cmd.get("text", ""))) url = {"whatsapp": f"https://web.whatsapp.com/send?text={text}", "telegram": f"tg://msg?text={text}", "sms": f"sms:?body={text}", "teams": "msteams:", "slack": "slack://open", "signal": "signal://"}.get(app, "") return "ok", (_open(url) if url else "unknown app") + f" -> to {cmd.get('contact')}: {cmd.get('text')}" if name == "find_in_page": st, o = _key("ctrl+f", plat) if st == "ok": _shell("sh" if plat != "win" else "ps", (f"xdotool type {q_sh(cmd.get('query',''))}" if plat != "win" else f"Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait({q_ps(cmd.get('query',''))})")) return st, f"find: {cmd.get('query')} ({o})" if name == "repeat_last": lp = _last["plan"] return ("ok", "repeated: " + lp.command) if lp else ("error", "nothing to repeat") return "error", f"unknown builtin {name}" def run(p, cmd=None, dry_run=True): if not p.supported: return {"status": "unsupported", "output": p.note, "command": p.command} if p.action == "clarify": return {"status": "clarify", "output": "ask the user: " + (p.note or "need more information"), "command": ""} if dry_run: return {"status": "dry_run", "output": "not executed (dry run)", "command": p.command} cmd = cmd or {} if p.kind == "py": st, out = _builtin(p.command or CMD[p.action][p.platform].split(":", 1)[1], cmd, p.platform) elif p.kind == "open": st, out = "ok", _open(p.command) elif p.kind == "key": st, out = _key(p.command, p.platform) else: st, out = _shell(p.kind, p.command) if st == "ok" and p.action != "repeat_last": _last["plan"] = p return {"status": st, "output": out, "command": p.command}