diff --git a/SKILL.md b/SKILL.md index 5255812..efd5073 100644 --- a/SKILL.md +++ b/SKILL.md @@ -376,6 +376,27 @@ Request Id. Full field mapping, prerequisites, and the exact browser steps are i Because the files upload from local disk, commit each component's `.SchLib`/`.PcbLib`/datasheet to the operator's machine (device bridge) first, and put those local paths in the manifest. +### Fully automatic end-of-run (no operator action) + +To make the Part Request the automatic tail of every run, the end task doesn't drive the browser +from the cloud (it can't reach the operator's desktop Chrome). Instead: + +1. After the Gitea push, the skill **writes `part_requests.json`** for the component(s) processed + this run, and **commits it plus each component's `.SchLib`/`.PcbLib`/datasheet to an *inbox + folder on the operator's machine*** via the device bridge (e.g. + `.../altium-library-master/_part_request_inbox/`). Use the local file paths (under that inbox) + in the manifest's `files`. +2. A one-time **local runner** on the operator's Windows machine (`scripts/local_runner.py`, set + up as a Task Scheduler job at logon or a startup shortcut, configured via + `runner_config.json`) watches that inbox, ensures Chrome is up with the debug port on the + signed-in profile, runs `altium365_part_request.py` on the manifest, and archives it. + +So each run ends with the skill dropping the manifest+files into the inbox; the local runner +submits the Part Request on its own. The only standing requirements are that the operator's +machine is on and its Chrome profile has been signed into the Workspace at least once (the runner +relaunches Chrome with the debug port and the persisted session is reused). This keeps the +per-component Claude-token cost at zero for the submission step. + ## Per-typeid versioning Versioning is **per typeid**, not global. Each typeid carries its own `template_version` and @@ -542,6 +563,10 @@ plain flat push, but it does not merge the changelog or blank the token, so pref - `scripts/altium365_part_request.py` — standalone **Selenium** submitter: attaches to the operator's signed-in Chrome and loops the `part_requests.json` manifest, filling and saving each Part Request. Token-free per component; the token-economical end task for a whole library. +- `scripts/local_runner.py` (+ `runner_config.example.json`) — the operator installs this **once** + on their machine; it watches the inbox for manifests the skill drops there, ensures Chrome's + debug session, runs the Selenium submitter, and archives each manifest — making submission the + automatic, hands-off tail of every run. - `assets/template/versions.json` — per-typeid `template_version` + `skill_version`. - `assets/CHANGELOG.xlsx` — global version/parameter changelog (created on first add; merged into the skill repo's copy in Gitea by `push-skill`). diff --git a/assets/CHANGELOG.xlsx b/assets/CHANGELOG.xlsx index 39139a0..ad4994c 100644 Binary files a/assets/CHANGELOG.xlsx and b/assets/CHANGELOG.xlsx differ diff --git a/scripts/local_runner.py b/scripts/local_runner.py new file mode 100644 index 0000000..7020839 --- /dev/null +++ b/scripts/local_runner.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""LOCAL RUNNER — runs on the operator's own machine (Windows), set up ONCE, to make Altium 365 +Part Request submission the automatic tail of a skill run. + +Why this exists: the library-manager skill runs in the cloud, but the Part Request web form must +be driven from the operator's signed-in Chrome on their desktop — the cloud can't reach it. So at +the end of a run the skill drops the finished files + a `part_requests.json` manifest into an +**inbox folder on this machine** (via the device bridge). This runner watches that inbox and, for +each manifest: makes sure Chrome is running with a debugging port on the operator's profile +(launching it if needed — the persisted login is reused), runs `altium365_part_request.py`, then +moves the manifest to a `processed/` folder. + +Set it up once (Task Scheduler at logon, or a startup shortcut) and every future skill run +auto-submits its Part Requests with no further action — as long as this machine is on and the +Chrome profile has been signed into the Workspace at least once. + +Config: a `runner_config.json` next to this file, e.g. +{ + "base": "https://vecmocon-technologies-pvt-ltd.365.altium.com", + "inbox": "D:/User Data/Desktop/altium-library-master/_part_request_inbox", + "processed": "D:/User Data/Desktop/altium-library-master/_part_request_inbox/processed", + "chrome": "C:/Program Files/Google/Chrome/Application/chrome.exe", + "user_data_dir": "%LOCALAPPDATA%/Google/Chrome/User Data", + "debug_port": 9222, + "chromedriver": null, + "review_first": false, + "poll_seconds": 30 +} + +Run: + python local_runner.py # watch the inbox forever (use for Task Scheduler / startup) + python local_runner.py --once # process whatever is in the inbox now, then exit +""" +import glob, json, os, shutil, socket, subprocess, sys, time + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def load_cfg(): + with open(os.path.join(HERE, "runner_config.json"), encoding="utf-8") as f: + return json.load(f) + + +def _port_open(port): + s = socket.socket(); s.settimeout(1) + try: + s.connect(("127.0.0.1", int(port))); return True + except Exception: + return False + finally: + s.close() + + +def ensure_chrome(cfg): + """Make sure a Chrome with the debug port is up (reusing the operator's logged-in profile).""" + if _port_open(cfg["debug_port"]): + return + udd = os.path.expandvars(cfg["user_data_dir"]) + subprocess.Popen([cfg["chrome"], f"--remote-debugging-port={cfg['debug_port']}", + f"--user-data-dir={udd}"]) + for _ in range(40): + if _port_open(cfg["debug_port"]): + time.sleep(2) # give the profile a moment to restore the session + return + time.sleep(1) + raise RuntimeError("Chrome did not expose a debug port — check 'chrome' path and 'debug_port'.") + + +def process(cfg, manifest): + ensure_chrome(cfg) + cmd = [sys.executable, os.path.join(HERE, "altium365_part_request.py"), + "--manifest", manifest, "--base", cfg["base"], + "--debugger-address", f"127.0.0.1:{cfg['debug_port']}", + "--out", manifest + ".result.json"] + if cfg.get("chromedriver"): + cmd += ["--chromedriver", cfg["chromedriver"]] + if cfg.get("review_first"): + cmd += ["--review-first"] + subprocess.run(cmd, check=False) + + +def main(): + cfg = load_cfg() + os.makedirs(cfg["inbox"], exist_ok=True) + os.makedirs(cfg["processed"], exist_ok=True) + once = "--once" in sys.argv + print(f"watching {cfg['inbox']} (base {cfg['base']})") + while True: + for m in sorted(glob.glob(os.path.join(cfg["inbox"], "*.json"))): + if m.endswith(".result.json"): + continue + print("processing", m) + try: + process(cfg, m) + except Exception as e: + print(" error:", e) + shutil.move(m, os.path.join(cfg["processed"], os.path.basename(m))) + if once: + break + time.sleep(int(cfg.get("poll_seconds", 30))) + + +if __name__ == "__main__": + main() diff --git a/scripts/runner_config.example.json b/scripts/runner_config.example.json new file mode 100644 index 0000000..6f79490 --- /dev/null +++ b/scripts/runner_config.example.json @@ -0,0 +1,11 @@ +{ + "base": "https://vecmocon-technologies-pvt-ltd.365.altium.com", + "inbox": "D:/User Data/Desktop/altium-library-master/_part_request_inbox", + "processed": "D:/User Data/Desktop/altium-library-master/_part_request_inbox/processed", + "chrome": "C:/Program Files/Google/Chrome/Application/chrome.exe", + "user_data_dir": "%LOCALAPPDATA%/Google/Chrome/User Data", + "debug_port": 9222, + "chromedriver": null, + "review_first": false, + "poll_seconds": 30 +}