@melaya/runner 1.1.62 → 1.1.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,9 +3,18 @@
3
3
  * folders) and runner:vault-pick-folder (Obsidian vaults).
4
4
  *
5
5
  * Browsers can't reveal absolute paths for security, so the runner — which
6
- * lives on the user's box — pops the dialog. Mac: osascript. Windows:
7
- * PowerShell WinForms. Linux: zenity. Fixed argv; the prompt is a constant
8
- * chosen by the caller, never user or model input.
6
+ * lives on the user's box — pops the dialog. Fixed argv; the prompt is a
7
+ * constant chosen by the caller, never user or model input.
8
+ *
9
+ * - Windows: the Explorer folder dialog (IFileOpenDialog + FOS_PICKFOLDERS)
10
+ * through PowerShell. WinForms' FolderBrowserDialog is the old tree view
11
+ * on Windows PowerShell (.NET Framework), so it is only the fallback. The
12
+ * dialog is owned by a small topmost window so it opens in front of the
13
+ * browser instead of behind it.
14
+ * - macOS: AppleScript `choose folder` run by osascript itself. Wrapping it
15
+ * in `tell application "System Events"` needs the Automation permission
16
+ * and fails (-1743) or hides the dialog when it is not granted.
17
+ * - Linux: zenity, then kdialog.
9
18
  */
10
19
  export type FolderPickResult = {
11
20
  path: string;
@@ -3,64 +3,134 @@
3
3
  * folders) and runner:vault-pick-folder (Obsidian vaults).
4
4
  *
5
5
  * Browsers can't reveal absolute paths for security, so the runner — which
6
- * lives on the user's box — pops the dialog. Mac: osascript. Windows:
7
- * PowerShell WinForms. Linux: zenity. Fixed argv; the prompt is a constant
8
- * chosen by the caller, never user or model input.
6
+ * lives on the user's box — pops the dialog. Fixed argv; the prompt is a
7
+ * constant chosen by the caller, never user or model input.
8
+ *
9
+ * - Windows: the Explorer folder dialog (IFileOpenDialog + FOS_PICKFOLDERS)
10
+ * through PowerShell. WinForms' FolderBrowserDialog is the old tree view
11
+ * on Windows PowerShell (.NET Framework), so it is only the fallback. The
12
+ * dialog is owned by a small topmost window so it opens in front of the
13
+ * browser instead of behind it.
14
+ * - macOS: AppleScript `choose folder` run by osascript itself. Wrapping it
15
+ * in `tell application "System Events"` needs the Automation permission
16
+ * and fails (-1743) or hides the dialog when it is not granted.
17
+ * - Linux: zenity, then kdialog.
9
18
  */
10
19
  import { execFile } from "child_process";
11
- export async function pickNativeFolder(prompt) {
12
- const platform = process.platform;
13
- const run = (cmd, args) => new Promise((resolve, reject) => {
14
- execFile(cmd, args, { maxBuffer: 1024 * 64 }, (err, stdout, stderr) => {
20
+ // Selected path on stdout, nothing on cancel. Paths are written as UTF-8 so
21
+ // non-ASCII folder names survive the console code page.
22
+ const WINDOWS_PICKER = String.raw `
23
+ $ErrorActionPreference = 'Stop'
24
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
25
+ $src = @"
26
+ using System;
27
+ using System.Drawing;
28
+ using System.Runtime.InteropServices;
29
+ using System.Windows.Forms;
30
+ public static class MelFolderPicker {
31
+ [ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")] class FileOpenDialogCoClass {}
32
+ [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
33
+ interface IShellItem {
34
+ void BindToHandler(); void GetParent();
35
+ void GetDisplayName(uint sigdn, [MarshalAs(UnmanagedType.LPWStr)] out string name);
36
+ void GetAttributes(); void Compare();
37
+ }
38
+ [ComImport, Guid("D57C7288-D4AD-4768-BE02-9D969532D960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
39
+ interface IFileOpenDialog {
40
+ [PreserveSig] int Show(IntPtr owner);
41
+ void SetFileTypes(); void SetFileTypeIndex(); void GetFileTypeIndex(); void Advise(); void Unadvise();
42
+ void SetOptions(uint fos); void GetOptions(out uint fos);
43
+ void SetDefaultFolder(); void SetFolder(); void GetFolder(); void GetCurrentSelection();
44
+ void SetFileName(); void GetFileName();
45
+ void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string title);
46
+ void SetOkButtonLabel(); void SetFileNameLabel();
47
+ void GetResult(out IShellItem item);
48
+ }
49
+ public static string Pick(string title) {
50
+ var owner = new Form {
51
+ TopMost = true, ShowInTaskbar = false, FormBorderStyle = FormBorderStyle.None,
52
+ StartPosition = FormStartPosition.CenterScreen, Size = new Size(1, 1), Opacity = 0,
53
+ };
54
+ owner.Show(); owner.Activate();
55
+ try {
56
+ var dlg = (IFileOpenDialog)new FileOpenDialogCoClass();
57
+ uint fos; dlg.GetOptions(out fos);
58
+ dlg.SetOptions(fos | 0x20 | 0x40 | 0x800); // PICKFOLDERS | FORCEFILESYSTEM | PATHMUSTEXIST
59
+ dlg.SetTitle(title);
60
+ if (dlg.Show(owner.Handle) != 0) return ""; // cancelled
61
+ IShellItem item; dlg.GetResult(out item);
62
+ string path; item.GetDisplayName(0x80058000, out path); // SIGDN_FILESYSPATH
63
+ return path ?? "";
64
+ } finally { owner.Close(); }
65
+ }
66
+ }
67
+ "@
68
+ Add-Type -TypeDefinition $src -ReferencedAssemblies System.Windows.Forms, System.Drawing
69
+ try {
70
+ [Console]::Out.Write([MelFolderPicker]::Pick($env:MEL_PICK_TITLE))
71
+ } catch {
72
+ # Very old shells without the COM dialog: the WinForms tree dialog.
73
+ Add-Type -AssemblyName System.Windows.Forms
74
+ $f = New-Object System.Windows.Forms.FolderBrowserDialog
75
+ $f.Description = $env:MEL_PICK_TITLE
76
+ $f.ShowNewFolderButton = $true
77
+ if ($f.ShowDialog() -eq 'OK') { [Console]::Out.Write($f.SelectedPath) }
78
+ }
79
+ `;
80
+ function run(cmd, args, env, exit1IsCancel = false) {
81
+ return new Promise((resolve, reject) => {
82
+ execFile(cmd, args, { maxBuffer: 1024 * 64, encoding: "utf8", windowsHide: false, env: env ?? process.env }, (err, stdout, stderr) => {
83
+ const out = (stdout || "").trim();
15
84
  if (err) {
16
- const out = (stdout || "").trim();
17
85
  const errMsg = (stderr || err.message || "").trim();
18
- // osascript returns code 1 when user cancels — empty stdout
19
- if (!out && /User canceled|cancelled/i.test(errMsg))
86
+ // osascript: "User canceled. (-128)"; zenity / kdialog: exit 1, no output.
87
+ if (!out && (/User cancel+ed|\(-128\)/i.test(errMsg) || (exit1IsCancel && err.code === 1)))
20
88
  return resolve("");
21
89
  return reject(new Error(errMsg || "pick failed"));
22
90
  }
23
- resolve((stdout || "").trim());
91
+ resolve(out);
24
92
  });
25
93
  });
26
- // Quotes would break out of the AppleScript / PowerShell string literal.
94
+ }
95
+ export async function pickNativeFolder(prompt) {
96
+ // Quotes and backslashes would break out of the AppleScript string literal.
27
97
  const safePrompt = prompt.replace(/["'`$\\]/g, "");
28
98
  let picked = "";
29
99
  try {
30
- if (platform === "darwin") {
31
- // osascript returns "Macintosh HD:Users:me:Documents" — convert to POSIX
32
- const raw = await run("osascript", [
33
- "-e", 'tell application "System Events"',
100
+ if (process.platform === "darwin") {
101
+ // `activate` brings osascript's own dialog to the front; the result is a
102
+ // POSIX path ("/Users/me/Vault/").
103
+ picked = await run("osascript", [
34
104
  "-e", "activate",
35
- "-e", `set p to choose folder with prompt "${safePrompt}"`,
36
- "-e", "end tell",
37
- "-e", "return POSIX path of p",
105
+ "-e", `POSIX path of (choose folder with prompt "${safePrompt}")`,
38
106
  ]);
39
- picked = raw.replace(/\/$/, "");
107
+ picked = picked.replace(/\/$/, "");
40
108
  }
41
- else if (platform === "win32") {
42
- const ps = [
43
- "Add-Type -AssemblyName System.Windows.Forms;",
44
- "$f = New-Object System.Windows.Forms.FolderBrowserDialog;",
45
- `$f.Description = '${safePrompt}';`,
46
- "$f.ShowNewFolderButton = $true;",
47
- "if ($f.ShowDialog() -eq 'OK') { [Console]::Out.Write($f.SelectedPath) }",
48
- ].join(" ");
49
- picked = await run("powershell", ["-NoProfile", "-STA", "-Command", ps]);
109
+ else if (process.platform === "win32") {
110
+ // The title goes through the environment, never into the script text.
111
+ const encoded = Buffer.from(WINDOWS_PICKER, "utf16le").toString("base64");
112
+ picked = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-STA", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], { ...process.env, MEL_PICK_TITLE: safePrompt });
50
113
  }
51
114
  else {
52
- // Linux — try zenity, fall back to error if missing
53
- picked = await run("zenity", [
54
- "--file-selection", "--directory",
55
- `--title=${safePrompt}`,
56
- ]);
115
+ try {
116
+ picked = await run("zenity", ["--file-selection", "--directory", `--title=${safePrompt}`], undefined, true);
117
+ }
118
+ catch (e) {
119
+ if (!/ENOENT/i.test(String(e?.message)))
120
+ throw e;
121
+ picked = await run("kdialog", ["--getexistingdirectory", process.env.HOME || "/", "--title", safePrompt], undefined, true);
122
+ }
57
123
  }
58
124
  }
59
125
  catch (e) {
60
126
  const msg = String(e?.message || "pick failed");
61
- const hint = /ENOENT|not found|spawn/i.test(msg) && process.platform === "linux"
62
- ? "Install 'zenity' (apt install zenity) or type the path manually."
63
- : "";
127
+ let hint = "";
128
+ if (process.platform === "linux" && /ENOENT|not found|spawn/i.test(msg)) {
129
+ hint = "Install 'zenity' (apt install zenity) or type the path manually.";
130
+ }
131
+ else if (process.platform === "darwin" && /-1743|not allowed|No user interaction/i.test(msg)) {
132
+ hint = "macOS blocked the dialog: run the runner from a logged-in session (Terminal), or type the path manually.";
133
+ }
64
134
  throw new Error(hint ? `${msg} — ${hint}` : msg);
65
135
  }
66
136
  if (!picked)
@@ -45,6 +45,20 @@ import urllib.request
45
45
  from pathlib import Path
46
46
  from typing import Any, Callable, Iterable
47
47
 
48
+
49
+ def _runner_user_agent() -> str:
50
+ # Cloudflare's browser integrity check refuses urllib's default
51
+ # "Python-urllib/x.y" agent (403, error 1010) before the request reaches
52
+ # Melaya, so every upload must name the runner.
53
+ try:
54
+ pkg = json.loads((Path(__file__).resolve().parent / "package.json").read_text(encoding="utf-8"))
55
+ return f"melaya-runner/{pkg.get('version') or 'dev'} (vault-mirror)"
56
+ except Exception:
57
+ return "melaya-runner (vault-mirror)"
58
+
59
+
60
+ _USER_AGENT = _runner_user_agent()
61
+
48
62
  CONFIG_FILES = (
49
63
  "app.json", "daily-notes.json", "templates.json", "zk-prefixer.json", "bookmarks.json",
50
64
  "types.json", "workspace.json", "workspaces.json", "core-plugins.json",
@@ -95,7 +109,7 @@ class HttpTransport:
95
109
  url = f"{self.base}/{rel}"
96
110
  if params:
97
111
  url += "?" + urllib.parse.urlencode(params)
98
- h = {"Authorization": f"Mirror {self._token}", **(headers or {})}
112
+ h = {"Authorization": f"Mirror {self._token}", "User-Agent": _USER_AGENT, **(headers or {})}
99
113
  body = data
100
114
  if json_body is not None:
101
115
  body = json.dumps(json_body).encode("utf-8")
@@ -45,6 +45,20 @@ import urllib.request
45
45
  from pathlib import Path
46
46
  from typing import Any, Callable, Iterable
47
47
 
48
+
49
+ def _runner_user_agent() -> str:
50
+ # Cloudflare's browser integrity check refuses urllib's default
51
+ # "Python-urllib/x.y" agent (403, error 1010) before the request reaches
52
+ # Melaya, so every upload must name the runner.
53
+ try:
54
+ pkg = json.loads((Path(__file__).resolve().parent / "package.json").read_text(encoding="utf-8"))
55
+ return f"melaya-runner/{pkg.get('version') or 'dev'} (vault-mirror)"
56
+ except Exception:
57
+ return "melaya-runner (vault-mirror)"
58
+
59
+
60
+ _USER_AGENT = _runner_user_agent()
61
+
48
62
  CONFIG_FILES = (
49
63
  "app.json", "daily-notes.json", "templates.json", "zk-prefixer.json", "bookmarks.json",
50
64
  "types.json", "workspace.json", "workspaces.json", "core-plugins.json",
@@ -95,7 +109,7 @@ class HttpTransport:
95
109
  url = f"{self.base}/{rel}"
96
110
  if params:
97
111
  url += "?" + urllib.parse.urlencode(params)
98
- h = {"Authorization": f"Mirror {self._token}", **(headers or {})}
112
+ h = {"Authorization": f"Mirror {self._token}", "User-Agent": _USER_AGENT, **(headers or {})}
99
113
  body = data
100
114
  if json_body is not None:
101
115
  body = json.dumps(json_body).encode("utf-8")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.1.62",
3
+ "version": "1.1.64",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Melaya Labs LLC",