@alfe.ai/openclaw-linkedin 0.1.0

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/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # LinkedIn local connection
2
+
3
+ This optional OpenClaw plugin uses the browser already owned by Alfe's Headless
4
+ Browser integration. It is browser-session access, **not LinkedIn OAuth or an
5
+ approved LinkedIn API integration**. LinkedIn can change its UI, limit activity,
6
+ challenge login, or restrict an account. Running locally does not remove that
7
+ risk. Future approved OAuth capabilities are separate; an OAuth token cannot
8
+ replace these browser cookies or imply inbox permission.
9
+
10
+ ## Installation and readiness
11
+
12
+ Install the LinkedIn integration through the normal integration lifecycle. Its
13
+ dependency is `headless-browser`; its setup hook invokes:
14
+
15
+ ```sh
16
+ npx -y @alfe.ai/openclaw-linkedin@0.1.0 setup
17
+ npx -y @alfe.ai/openclaw-linkedin@0.1.0 health
18
+ ```
19
+
20
+ The initial local runtime supports **Linux and macOS**. Other platforms fail
21
+ closed before spawning because verified driver-process teardown is required.
22
+
23
+ Setup uses `uv`, already provisioned on Alfe managed machines, to obtain Python
24
+ 3.13 and create a private environment under `~/.alfe/linkedin/`. Self-hosted
25
+ machines can install uv first or supply an existing Python with venv and
26
+ `waitid`/`WNOWAIT` support: Linux 3.12.4–3.14 or macOS 3.13–3.14
27
+ (`setup --python /absolute/path/to/python`). Setup and health check these
28
+ supervisor capabilities before importing dependencies. Setup, health and each
29
+ tool invocation also validate `/bin/ps -axo pgid=,stat=` with a five-second
30
+ inspection timeout before any browser control is acquired. Direct dependencies are pinned
31
+ to `mcp-server-linkedin==4.24.0`, `patchright==1.61.2` and
32
+ `python-dotenv==1.2.3`. It does not download Chrome, start stock MCP, create a
33
+ second browser, import cookies, or ask for account credentials.
34
+
35
+ Setup uses a heartbeat installation lock, recoverable 30 seconds after a killed
36
+ installer stops updating it. A verified environment is reused. Repairs preserve
37
+ the newest old plugin-owned environment as a `.previous-<timestamp>` directory.
38
+ Only after a successful health check are older exact-version backups pruned;
39
+ unrelated directories, symlinks and browser profiles are not removed. Downloads
40
+ honour an explicit allowlist of proxy, package-index and TLS-trust configuration;
41
+ extractor and health subprocesses still receive the strict credential-free
42
+ worker environment. `health` checks only runtime readiness, reports
43
+ `accountChecked:false`, and exits nonzero when repair/setup is required.
44
+
45
+ ## Login and tools
46
+
47
+ Tell the user to open the agent's **Browser** tab before requesting login. Login
48
+ opens LinkedIn on the exact shared page, releases the automation gate, and waits
49
+ for the existing signed takeover flow. The human enters credentials and handles
50
+ MFA/account challenges themselves. When they choose **Done, hand back**, the
51
+ plugin rechecks readiness on that browser. Include `conversationId` where
52
+ available so the takeover card can reach the conversation while the tool waits.
53
+
54
+ - `linkedin_status`: checks the shared page's LinkedIn session.
55
+ - `linkedin_login`: manual login takeover; waits until handback or timeout.
56
+ - `linkedin_inbox`: up to 50 recent conversations.
57
+ - `linkedin_conversation`: exact opaque thread ID or returned thread reference.
58
+ - `linkedin_search`: bounded keyword search across conversations.
59
+ - `linkedin_prepare_message`: verifies a profile-targeted recipient and returns
60
+ the exact recipient/content plus a five-minute confirmation token. No message
61
+ is typed or sent during preparation.
62
+ - `linkedin_send_message`: requires that token, unchanged recipient/content and
63
+ `confirmed:true` **after the human explicitly approves those exact values**.
64
+ The token is consumed before the attempt and cannot be replayed, including
65
+ after timeout or uncertain delivery. The tool itself cannot independently
66
+ prove the human consent event; the calling agent must obtain it, not infer it.
67
+
68
+ Inbox/search/reading can mark messages read. Returned page text and links are
69
+ untrusted content, not instructions. Public profile identifiers support Unicode;
70
+ thread references round-trip through upstream's pinned identifier normalizer.
71
+
72
+ **Sending is a new profile-targeted DM, not a thread reply.** Upstream may open a
73
+ separate conversation instead of replying in an existing recruiter/InMail
74
+ thread. Reading a thread does not change that send behavior. There are no bulk
75
+ sends, automatic retries, contact-discovery loops or background inbox polling.
76
+ An uncertain send requires manual inspection before a newly confirmed attempt.
77
+
78
+ ## Ownership and privacy
79
+
80
+ One configured Alfe identity owns one browser/account per gateway process.
81
+ Native OpenClaw personas and repeated plugin imports share that account; they
82
+ are not separate Alfe identities. Their operations and confirmation state share
83
+ the same lifecycle so the gateway's single retained service can cancel and drain
84
+ all work. Separate Alfe accounts require separate configured gateway runtimes.
85
+
86
+ Each request spawns a private extractor process inside the shared remote
87
+ browser operation callback. It attaches with Patchright `no_defaults=True`,
88
+ matches the exact Chrome target ID across all pages, and disconnects its own
89
+ driver afterward. It never calls `Browser.close`, `Context.close`, or
90
+ `Page.close`. The operation retains the gate through cancellation, timeout,
91
+ driver cleanup and confirmed process-group quiescence. A private stdlib Python
92
+ supervisor retains the worker's PID unreaped until its group is drained, so a
93
+ recycled PID cannot cause an unrelated process to be signalled. Node requests
94
+ cancellation over a supervisor-only pipe and waits for a cleanup acknowledgement
95
+ plus process close. Plugin service stop also
96
+ cancels and drains a pending human takeover. No automation runs during login.
97
+
98
+ If that supervisor dies unexpectedly, the browser remains locked because worker
99
+ cleanup cannot be confirmed. Recovery requires restarting the full agent/browser
100
+ runtime, including old Chrome and any surviving workers (a managed VM reboot is
101
+ sufficient). Reloading only this plugin must not resume browser access.
102
+
103
+ Only the trusted browser owner supplies the private CDP endpoint; tools cannot
104
+ select a browser, target, interpreter or script. Endpoints, raw exceptions and
105
+ subprocess stderr do not reach tool results. Ambient credentials/proxy/startup
106
+ variables are stripped, dotenv loading is disabled, and upstream trace capture
107
+ is forced off before import. Cookies remain solely in Alfe's existing browser
108
+ profile. Message confirmation state is bounded, memory-only, single-use and
109
+ cleared on stop; this plugin does not add a cloud credential/token store.
110
+
111
+ ## Verification
112
+
113
+ ```sh
114
+ pnpm --filter @alfe.ai/openclaw-linkedin lint
115
+ pnpm --filter @alfe.ai/openclaw-linkedin typecheck
116
+ pnpm --filter @alfe.ai/openclaw-linkedin test
117
+ python -m unittest discover -s packages/openclaw-linkedin/python -p 'test_*.py'
118
+ pnpm --filter @alfe.ai/openclaw-linkedin build
119
+ LINKEDIN_TEST_PYTHON=/path/to/private/python pnpm --filter @alfe.ai/openclaw-linkedin test:integration
120
+ ```
121
+
122
+ Python tests require the pinned requirements installed in that interpreter.
123
+ Integration tests use fresh Chrome profiles and a synthetic local website, with
124
+ real Patchright attachment and runtime ownership. They do not authenticate a
125
+ real LinkedIn account or send any real message. LinkedIn's live UI compatibility,
126
+ provider approval, and account eligibility are outside those tests' claims.
@@ -0,0 +1,24 @@
1
+ # Third-party dependencies
2
+
3
+ The private Python environment installs the upstream packages from their
4
+ published distributions. Their license notices remain in those distributions;
5
+ Alfe does not redistribute a modified upstream browser launcher or MCP server.
6
+
7
+ - **linkedin-mcp-server / mcp-server-linkedin 4.24.0** — Daniel Sticker and
8
+ contributors, Apache License 2.0.
9
+ Source: https://github.com/stickerdaniel/linkedin-mcp-server
10
+ Reviewed tag commit: `a29e809eabeb4f4f641b3d1d7cce0f51202a47b2`.
11
+ License: https://github.com/stickerdaniel/linkedin-mcp-server/blob/v4.24.0/LICENSE
12
+ - **Patchright 1.61.2** — Patchright contributors, Apache License 2.0.
13
+ Source: https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python
14
+ - **python-dotenv 1.2.3** — python-dotenv contributors, BSD-3-Clause.
15
+ Source: https://github.com/theskumar/python-dotenv
16
+ - **proper-lockfile 4.1.2** — IndigoUnited and contributors, MIT.
17
+ Source: https://github.com/moxystudio/node-proper-lockfile
18
+
19
+ The adapter calls `LinkedInExtractor` on a caller-owned page. Its only extractor
20
+ override disables automatic saved-account chooser handling, preserving manual
21
+ authentication through Alfe. See `python/worker.py` and the contract tests when
22
+ updating an upstream pin. A future version must be reviewed for browser
23
+ ownership, tracing, authentication side effects, messaging semantics and license
24
+ changes before its pin is changed.
package/dist/cli.cjs ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ const require_installer = require("./installer.cjs");
3
+ //#region src/cli-errors.ts
4
+ /** Only our fixed, actionable installer messages are safe to print verbatim. */
5
+ function runtimeFailureMessage(error, command) {
6
+ if (error instanceof require_installer.InstallerError) return error.message;
7
+ if (command === "health") return "LinkedIn runtime health check failed. Verify private Python dependencies and /bin/ps process inspection, then rerun health.";
8
+ return "LinkedIn runtime setup failed. Check private-runtime permissions, Python/uv and package-download configuration, then rerun setup.";
9
+ }
10
+ //#endregion
11
+ //#region src/cli.ts
12
+ async function main() {
13
+ const [command, ...args] = process.argv.slice(2);
14
+ if (command === "--help" || command === "help" || !command) {
15
+ process.stdout.write("Usage: alfe-linkedin setup [--python /absolute/python] | health\nSetup installs private pinned extraction dependencies only, never a browser or account session.\n");
16
+ return;
17
+ }
18
+ if (command === "health" && args.length === 0) {
19
+ const ready = await require_installer.checkRuntime();
20
+ process.stdout.write(`${JSON.stringify({
21
+ status: ready ? "ready" : "runtime_missing",
22
+ accountChecked: false
23
+ })}\n`);
24
+ process.exitCode = ready ? 0 : 1;
25
+ return;
26
+ }
27
+ if (command === "setup" && (args.length === 0 || args.length === 2 && args[0] === "--python" && args[1].startsWith("/"))) {
28
+ if (process.platform !== "linux" && process.platform !== "darwin") {
29
+ process.stderr.write("LinkedIn local currently supports Linux and macOS only.\n");
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+ process.umask(63);
34
+ await require_installer.setupRuntime(args.length ? { python: args[1] } : {});
35
+ process.stdout.write("{\"status\":\"ready\",\"accountChecked\":false}\n");
36
+ return;
37
+ }
38
+ process.stderr.write("Invalid arguments. Use alfe-linkedin --help.\n");
39
+ process.exitCode = 2;
40
+ }
41
+ main().catch((error) => {
42
+ process.stderr.write(`${runtimeFailureMessage(error, process.argv[2])}\n`);
43
+ process.exitCode = 1;
44
+ });
45
+ //#endregion
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { n as checkRuntime, r as setupRuntime, t as InstallerError } from "./installer.js";
3
+ //#region src/cli-errors.ts
4
+ /** Only our fixed, actionable installer messages are safe to print verbatim. */
5
+ function runtimeFailureMessage(error, command) {
6
+ if (error instanceof InstallerError) return error.message;
7
+ if (command === "health") return "LinkedIn runtime health check failed. Verify private Python dependencies and /bin/ps process inspection, then rerun health.";
8
+ return "LinkedIn runtime setup failed. Check private-runtime permissions, Python/uv and package-download configuration, then rerun setup.";
9
+ }
10
+ //#endregion
11
+ //#region src/cli.ts
12
+ async function main() {
13
+ const [command, ...args] = process.argv.slice(2);
14
+ if (command === "--help" || command === "help" || !command) {
15
+ process.stdout.write("Usage: alfe-linkedin setup [--python /absolute/python] | health\nSetup installs private pinned extraction dependencies only, never a browser or account session.\n");
16
+ return;
17
+ }
18
+ if (command === "health" && args.length === 0) {
19
+ const ready = await checkRuntime();
20
+ process.stdout.write(`${JSON.stringify({
21
+ status: ready ? "ready" : "runtime_missing",
22
+ accountChecked: false
23
+ })}\n`);
24
+ process.exitCode = ready ? 0 : 1;
25
+ return;
26
+ }
27
+ if (command === "setup" && (args.length === 0 || args.length === 2 && args[0] === "--python" && args[1].startsWith("/"))) {
28
+ if (process.platform !== "linux" && process.platform !== "darwin") {
29
+ process.stderr.write("LinkedIn local currently supports Linux and macOS only.\n");
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+ process.umask(63);
34
+ await setupRuntime(args.length ? { python: args[1] } : {});
35
+ process.stdout.write("{\"status\":\"ready\",\"accountChecked\":false}\n");
36
+ return;
37
+ }
38
+ process.stderr.write("Invalid arguments. Use alfe-linkedin --help.\n");
39
+ process.exitCode = 2;
40
+ }
41
+ main().catch((error) => {
42
+ process.stderr.write(`${runtimeFailureMessage(error, process.argv[2])}\n`);
43
+ process.exitCode = 1;
44
+ });
45
+ //#endregion
46
+ export {};
package/dist/index.cjs ADDED
@@ -0,0 +1,7 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_runtime = require("./runtime.cjs");
3
+ exports.WorkerFailure = require_runtime.WorkerFailure;
4
+ exports.createLinkedInPlugin = require_runtime.createLinkedInPlugin;
5
+ exports.createLinkedInRuntimeState = require_runtime.createLinkedInRuntimeState;
6
+ exports.parseWorkerResult = require_runtime.parseWorkerResult;
7
+ exports.runWorker = require_runtime.runWorker;
@@ -0,0 +1,2 @@
1
+ import { a as LinkedInTool, c as BrowserOperationContext, d as WorkerRequest, f as WorkerResult, i as LinkedInRuntimeState, l as WorkerFailure, m as runWorker, n as LinkedInPlugin, o as createLinkedInPlugin, p as parseWorkerResult, r as LinkedInPluginApi, s as createLinkedInRuntimeState, t as LinkedInDependencies, u as WorkerOptions } from "./runtime.cjs";
2
+ export { type BrowserOperationContext, type LinkedInDependencies, type LinkedInPlugin, type LinkedInPluginApi, type LinkedInRuntimeState, type LinkedInTool, WorkerFailure, type WorkerOptions, type WorkerRequest, type WorkerResult, createLinkedInPlugin, createLinkedInRuntimeState, parseWorkerResult, runWorker };
@@ -0,0 +1,2 @@
1
+ import { a as LinkedInTool, c as BrowserOperationContext, d as WorkerRequest, f as WorkerResult, i as LinkedInRuntimeState, l as WorkerFailure, m as runWorker, n as LinkedInPlugin, o as createLinkedInPlugin, p as parseWorkerResult, r as LinkedInPluginApi, s as createLinkedInRuntimeState, t as LinkedInDependencies, u as WorkerOptions } from "./runtime.js";
2
+ export { type BrowserOperationContext, type LinkedInDependencies, type LinkedInPlugin, type LinkedInPluginApi, type LinkedInRuntimeState, type LinkedInTool, WorkerFailure, type WorkerOptions, type WorkerRequest, type WorkerResult, createLinkedInPlugin, createLinkedInRuntimeState, parseWorkerResult, runWorker };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import { a as runWorker, i as parseWorkerResult, n as createLinkedInRuntimeState, r as WorkerFailure, t as createLinkedInPlugin } from "./runtime.js";
2
+ export { WorkerFailure, createLinkedInPlugin, createLinkedInRuntimeState, parseWorkerResult, runWorker };
@@ -0,0 +1,320 @@
1
+ let node_child_process = require("node:child_process");
2
+ let node_fs_promises = require("node:fs/promises");
3
+ let node_path = require("node:path");
4
+ let node_module = require("node:module");
5
+ let node_url = require("node:url");
6
+ let node_os = require("node:os");
7
+ //#region src/paths.ts
8
+ const EXTRACTOR_VERSION = "4.24.0";
9
+ const PATCHRIGHT_VERSION = "1.61.2";
10
+ const DOTENV_VERSION = "1.2.3";
11
+ const RUNTIME_ID = `runtime-${EXTRACTOR_VERSION}-patchright-${PATCHRIGHT_VERSION}-dotenv-${DOTENV_VERSION}`;
12
+ function stateDirectory() {
13
+ return (0, node_path.join)((0, node_os.homedir)(), ".alfe", "linkedin");
14
+ }
15
+ function runtimeDirectory() {
16
+ return (0, node_path.join)(stateDirectory(), RUNTIME_ID);
17
+ }
18
+ function runtimePython(directory = runtimeDirectory()) {
19
+ return (0, node_path.join)(directory, process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
20
+ }
21
+ function workerScript() {
22
+ return (0, node_url.fileURLToPath)(new URL("../python/worker.py", require("url").pathToFileURL(__filename).href));
23
+ }
24
+ /** No Alfe/provider tokens, interpreter injection, proxy credentials or traces. */
25
+ function workerEnvironment(source = process.env) {
26
+ const env = {};
27
+ for (const key of [
28
+ "PATH",
29
+ "SystemRoot",
30
+ "WINDIR",
31
+ "TEMP",
32
+ "TMP",
33
+ "TMPDIR",
34
+ "HOME",
35
+ "USERPROFILE",
36
+ "LANG",
37
+ "LC_ALL"
38
+ ]) if (source[key]) env[key] = source[key];
39
+ env.LINKEDIN_TRACE_MODE = "off";
40
+ env.LINKEDIN_DEBUG_TRACE_DIR = "";
41
+ env.PYTHON_DOTENV_DISABLED = "1";
42
+ env.PYTHONUNBUFFERED = "1";
43
+ env.PYTHONDONTWRITEBYTECODE = "1";
44
+ return env;
45
+ }
46
+ //#endregion
47
+ //#region src/installer-env.ts
48
+ const INSTALLER_NETWORK_KEYS = [
49
+ "HTTP_PROXY",
50
+ "HTTPS_PROXY",
51
+ "ALL_PROXY",
52
+ "NO_PROXY",
53
+ "http_proxy",
54
+ "https_proxy",
55
+ "all_proxy",
56
+ "no_proxy",
57
+ "PIP_INDEX_URL",
58
+ "PIP_EXTRA_INDEX_URL",
59
+ "PIP_NO_INDEX",
60
+ "PIP_FIND_LINKS",
61
+ "PIP_TRUSTED_HOST",
62
+ "PIP_CERT",
63
+ "PIP_CLIENT_CERT",
64
+ "PIP_CONFIG_FILE",
65
+ "PIP_PROXY",
66
+ "PIP_KEYRING_PROVIDER",
67
+ "PIP_TIMEOUT",
68
+ "PIP_RETRIES",
69
+ "UV_INDEX",
70
+ "UV_DEFAULT_INDEX",
71
+ "UV_INDEX_URL",
72
+ "UV_EXTRA_INDEX_URL",
73
+ "UV_NO_INDEX",
74
+ "UV_FIND_LINKS",
75
+ "UV_INDEX_STRATEGY",
76
+ "UV_INSECURE_HOST",
77
+ "UV_NATIVE_TLS",
78
+ "UV_SYSTEM_CERTS",
79
+ "UV_CONFIG_FILE",
80
+ "UV_CREDENTIALS_DIR",
81
+ "UV_KEYRING_PROVIDER",
82
+ "UV_HTTP_TIMEOUT",
83
+ "UV_HTTP_CONNECT_TIMEOUT",
84
+ "UV_HTTP_RETRIES",
85
+ "UV_PYTHON_INSTALL_MIRROR",
86
+ "UV_PYPY_INSTALL_MIRROR",
87
+ "SSL_CERT_FILE",
88
+ "SSL_CERT_DIR",
89
+ "SSL_CLIENT_CERT",
90
+ "REQUESTS_CA_BUNDLE",
91
+ "CURL_CA_BUNDLE",
92
+ "NETRC"
93
+ ];
94
+ /** Installer-only download settings; never use this environment for provider imports. */
95
+ function installerEnvironment(source = process.env) {
96
+ const env = workerEnvironment(source);
97
+ for (const key of INSTALLER_NETWORK_KEYS) if (source[key] !== void 0) env[key] = source[key];
98
+ for (const key of Object.keys(source)) if (/^UV_INDEX_[A-Z0-9_]+_(USERNAME|PASSWORD)$/.test(key) && source[key] !== void 0) env[key] = source[key];
99
+ return env;
100
+ }
101
+ //#endregion
102
+ //#region src/installer.ts
103
+ /** Only fixed, intentionally public installer guidance uses this error type. */
104
+ var InstallerError = class extends Error {
105
+ name = "InstallerError";
106
+ };
107
+ const PYTHON_COMPATIBLE = "import os, sys; assert (3,12,4) <= sys.version_info[:3] < (3,15,0); assert all(hasattr(os, name) for name in ('fork', 'setsid', 'waitid', 'WNOWAIT', 'WEXITED', 'WNOHANG', 'P_PID'))";
108
+ const VERIFY = `${PYTHON_COMPATIBLE}; import importlib.metadata as m; assert m.version('mcp-server-linkedin') == '${EXTRACTOR_VERSION}'; assert m.version('patchright') == '${PATCHRIGHT_VERSION}'; assert m.version('python-dotenv') == '${DOTENV_VERSION}'; from linkedin_mcp_server.scraping.extractor import LinkedInExtractor; from patchright.async_api import async_playwright; print('ready')`;
109
+ const SUPERVISOR_SCRIPT = (0, node_url.fileURLToPath)(new URL("../python/supervisor.py", require("url").pathToFileURL(__filename).href));
110
+ const lockfile = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("proper-lockfile");
111
+ function executeCommand(command, args, env, timeout) {
112
+ return new Promise((resolve) => {
113
+ (0, node_child_process.execFile)(command, args, {
114
+ env,
115
+ timeout,
116
+ maxBuffer: 512 * 1024,
117
+ encoding: "utf8"
118
+ }, (error, stdout) => {
119
+ resolve({
120
+ ok: !error,
121
+ stdout
122
+ });
123
+ });
124
+ });
125
+ }
126
+ const runCommand = (command, args) => executeCommand(command, args, installerEnvironment(), 18e4);
127
+ const runReadinessCommand = (command, args) => executeCommand(command, args, workerEnvironment(), 15e3);
128
+ async function checkRuntime(options = {}) {
129
+ const run = options.run ?? runReadinessCommand;
130
+ const python = options.python ?? runtimePython((0, node_path.join)(options.stateDir ?? stateDirectory(), RUNTIME_ID));
131
+ const supervisor = await run(python, [
132
+ "-I",
133
+ SUPERVISOR_SCRIPT,
134
+ "--check"
135
+ ]);
136
+ if (!supervisor.ok || supervisor.stdout.trim() !== "ready") return false;
137
+ const result = await run(python, [
138
+ "-I",
139
+ "-c",
140
+ VERIFY
141
+ ]);
142
+ return result.ok && result.stdout.trim() === "ready";
143
+ }
144
+ /** Retain the latest recoverable environment, without following or removing symlinks. */
145
+ async function prunePreviousRuntimes(stateDir, assertLease) {
146
+ assertLease();
147
+ const prefix = `${RUNTIME_ID}.previous-`;
148
+ const previous = (await (0, node_fs_promises.readdir)(stateDir, { withFileTypes: true })).flatMap((entry) => {
149
+ if (!entry.isDirectory() || !entry.name.startsWith(prefix)) return [];
150
+ const suffix = entry.name.slice(prefix.length);
151
+ const timestamp = Number(suffix);
152
+ if (!/^(0|[1-9]\d*)$/.test(suffix) || !Number.isSafeInteger(timestamp)) return [];
153
+ return [{
154
+ name: entry.name,
155
+ timestamp
156
+ }];
157
+ }).sort((left, right) => right.timestamp - left.timestamp);
158
+ for (const entry of previous.slice(1)) {
159
+ assertLease();
160
+ const path = (0, node_path.join)(stateDir, entry.name);
161
+ const metadata = await (0, node_fs_promises.lstat)(path).catch((error) => {
162
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
163
+ throw error;
164
+ });
165
+ if (metadata?.isDirectory() && !metadata.isSymbolicLink()) await (0, node_fs_promises.rm)(path, {
166
+ recursive: true,
167
+ force: true
168
+ });
169
+ }
170
+ }
171
+ /** One immutable dependency environment; never creates/downloads a browser or touches a profile. */
172
+ async function setupRuntime(options = {}) {
173
+ if (process.platform !== "linux" && process.platform !== "darwin") throw new InstallerError("LinkedIn local currently supports Linux and macOS only.");
174
+ const stateDir = options.stateDir ?? stateDirectory();
175
+ const run = options.run ?? runCommand;
176
+ const readinessRun = options.run ?? runReadinessCommand;
177
+ await (0, node_fs_promises.mkdir)(stateDir, {
178
+ recursive: true,
179
+ mode: 448
180
+ });
181
+ await (0, node_fs_promises.chmod)(stateDir, 448);
182
+ const lock = (0, node_path.join)(stateDir, "setup.lock");
183
+ let compromised = false;
184
+ const assertLease = () => {
185
+ if (compromised) throw new InstallerError("LinkedIn setup lost its installation lease. Check runtime health and retry setup after any other installation finishes.");
186
+ };
187
+ let release;
188
+ try {
189
+ release = await lockfile.lock(stateDir, {
190
+ lockfilePath: lock,
191
+ stale: 3e4,
192
+ update: 5e3,
193
+ retries: 0,
194
+ onCompromised: () => {
195
+ compromised = true;
196
+ }
197
+ });
198
+ } catch {
199
+ throw new InstallerError("LinkedIn setup is already running. Wait for it to finish before reinstalling.");
200
+ }
201
+ let staging;
202
+ try {
203
+ if (await checkRuntime({
204
+ stateDir,
205
+ run: readinessRun
206
+ })) {
207
+ await prunePreviousRuntimes(stateDir, assertLease);
208
+ return;
209
+ }
210
+ const uv = (await run("uv", ["--version"])).ok;
211
+ let python = options.python;
212
+ if (!uv || python) {
213
+ const candidates = python ? [python] : [
214
+ "python3.13",
215
+ "python3.14",
216
+ "python3.12",
217
+ "python3"
218
+ ];
219
+ python = void 0;
220
+ for (const candidate of candidates) if ((await run(candidate, [
221
+ "-I",
222
+ "-c",
223
+ PYTHON_COMPATIBLE
224
+ ])).ok) {
225
+ python = candidate;
226
+ break;
227
+ }
228
+ if (!python) throw new InstallerError("Install uv (provided on Alfe managed agents), or Python with venv and waitid/WNOWAIT support (Linux 3.12.4–3.14; macOS 3.13–3.14), then rerun LinkedIn setup.");
229
+ }
230
+ staging = await (0, node_fs_promises.mkdtemp)((0, node_path.join)(stateDir, `${RUNTIME_ID}.install-`));
231
+ if (!(uv ? await run("uv", [
232
+ "venv",
233
+ "--python",
234
+ python ?? "3.13",
235
+ staging
236
+ ]) : python ? await run(python, [
237
+ "-I",
238
+ "-m",
239
+ "venv",
240
+ staging
241
+ ]) : { ok: false }).ok) throw new InstallerError("LinkedIn setup could not create its private Python environment. Check Python/uv availability and retry setup.");
242
+ const executable = runtimePython(staging);
243
+ const packages = [
244
+ `mcp-server-linkedin==${EXTRACTOR_VERSION}`,
245
+ `patchright==${PATCHRIGHT_VERSION}`,
246
+ `python-dotenv==${DOTENV_VERSION}`
247
+ ];
248
+ if (!(uv ? await run("uv", [
249
+ "pip",
250
+ "install",
251
+ "--python",
252
+ executable,
253
+ ...packages
254
+ ]) : await run(executable, [
255
+ "-I",
256
+ "-m",
257
+ "pip",
258
+ "install",
259
+ "--disable-pip-version-check",
260
+ ...packages
261
+ ])).ok || !await checkRuntime({
262
+ python: executable,
263
+ run: readinessRun
264
+ })) throw new InstallerError("LinkedIn setup could not verify its pinned dependencies. Check package registry access and retry setup.");
265
+ assertLease();
266
+ const destination = (0, node_path.join)(stateDir, RUNTIME_ID);
267
+ if (await (0, node_fs_promises.access)(destination).then(() => true, () => false)) await (0, node_fs_promises.rename)(destination, (0, node_path.join)(stateDir, `${RUNTIME_ID}.previous-${String(Date.now())}`));
268
+ await (0, node_fs_promises.rename)(staging, destination);
269
+ staging = void 0;
270
+ await (0, node_fs_promises.chmod)(destination, 448);
271
+ if (!await checkRuntime({
272
+ stateDir,
273
+ run: readinessRun
274
+ })) throw new InstallerError("The installed LinkedIn runtime failed its final check. Retry setup to repair this private environment.");
275
+ await prunePreviousRuntimes(stateDir, assertLease);
276
+ } finally {
277
+ if (staging) await (0, node_fs_promises.rm)(staging, {
278
+ recursive: true,
279
+ force: true
280
+ });
281
+ await release();
282
+ }
283
+ }
284
+ //#endregion
285
+ Object.defineProperty(exports, "InstallerError", {
286
+ enumerable: true,
287
+ get: function() {
288
+ return InstallerError;
289
+ }
290
+ });
291
+ Object.defineProperty(exports, "checkRuntime", {
292
+ enumerable: true,
293
+ get: function() {
294
+ return checkRuntime;
295
+ }
296
+ });
297
+ Object.defineProperty(exports, "runtimePython", {
298
+ enumerable: true,
299
+ get: function() {
300
+ return runtimePython;
301
+ }
302
+ });
303
+ Object.defineProperty(exports, "setupRuntime", {
304
+ enumerable: true,
305
+ get: function() {
306
+ return setupRuntime;
307
+ }
308
+ });
309
+ Object.defineProperty(exports, "workerEnvironment", {
310
+ enumerable: true,
311
+ get: function() {
312
+ return workerEnvironment;
313
+ }
314
+ });
315
+ Object.defineProperty(exports, "workerScript", {
316
+ enumerable: true,
317
+ get: function() {
318
+ return workerScript;
319
+ }
320
+ });