@melaya/runner 1.1.81 → 1.1.83

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.1.81",
3
+ "version": "1.1.83",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Melaya Labs LLC",
@@ -1,51 +0,0 @@
1
- /**
2
- * LinkedIn Voyager-API egress for the @melaya/runner.
3
- *
4
- * The server (Alibaba) forwards LinkedIn API calls to the runner via
5
- * Socket.IO `linkedin:exec` events. This module:
6
- *
7
- * 1. Reads the operator's local cookies (li_at + JSESSIONID) from
8
- * ~/.melaya-runner/linkedin/cookies.json. The cookies were saved
9
- * via `npx @melaya/runner linkedin-setup`. They never leave this
10
- * machine.
11
- *
12
- * 2. Issues the actual HTTPS call to LinkedIn's Voyager API
13
- * (https://www.linkedin.com/voyager/api/...) using Node's native
14
- * fetch. The egress IP is the operator's residential IP — exactly
15
- * what they normally browse LinkedIn from, so no automation
16
- * flags.
17
- *
18
- * 3. For "write" actions (likes / replies / connection requests)
19
- * inserts a 3-9s human pause before the call, increments a daily
20
- * counter at ~/.melaya-runner/linkedin/usage-{YYYY-MM-DD}.json,
21
- * and refuses with `daily_cap_reached` past the configured cap.
22
- *
23
- * 4. Returns { status, body } back to the server so the caller (the
24
- * Python agent's social_linkedin tool) can parse the response.
25
- *
26
- * Caps (override via env on the runner):
27
- * LINKEDIN_DAILY_LIKES_CAP default 50
28
- * LINKEDIN_DAILY_REPLIES_CAP default 20
29
- * LINKEDIN_DAILY_CONNECTIONS_CAP default 15
30
- */
31
- interface LinkedInCookies {
32
- li_at: string;
33
- JSESSIONID: string;
34
- }
35
- export declare function readCookies(): LinkedInCookies | null;
36
- export declare function writeCookies(c: LinkedInCookies): string;
37
- export interface LinkedInExecRequest {
38
- correlation_id: string;
39
- method: string;
40
- path: string;
41
- params?: Record<string, unknown> | null;
42
- body?: unknown;
43
- write?: boolean;
44
- }
45
- export interface LinkedInExecResult {
46
- status?: number;
47
- body?: unknown;
48
- error?: string;
49
- }
50
- export declare function handleLinkedInExec(req: LinkedInExecRequest, verbose?: boolean): Promise<LinkedInExecResult>;
51
- export {};
@@ -1,216 +0,0 @@
1
- /**
2
- * LinkedIn Voyager-API egress for the @melaya/runner.
3
- *
4
- * The server (Alibaba) forwards LinkedIn API calls to the runner via
5
- * Socket.IO `linkedin:exec` events. This module:
6
- *
7
- * 1. Reads the operator's local cookies (li_at + JSESSIONID) from
8
- * ~/.melaya-runner/linkedin/cookies.json. The cookies were saved
9
- * via `npx @melaya/runner linkedin-setup`. They never leave this
10
- * machine.
11
- *
12
- * 2. Issues the actual HTTPS call to LinkedIn's Voyager API
13
- * (https://www.linkedin.com/voyager/api/...) using Node's native
14
- * fetch. The egress IP is the operator's residential IP — exactly
15
- * what they normally browse LinkedIn from, so no automation
16
- * flags.
17
- *
18
- * 3. For "write" actions (likes / replies / connection requests)
19
- * inserts a 3-9s human pause before the call, increments a daily
20
- * counter at ~/.melaya-runner/linkedin/usage-{YYYY-MM-DD}.json,
21
- * and refuses with `daily_cap_reached` past the configured cap.
22
- *
23
- * 4. Returns { status, body } back to the server so the caller (the
24
- * Python agent's social_linkedin tool) can parse the response.
25
- *
26
- * Caps (override via env on the runner):
27
- * LINKEDIN_DAILY_LIKES_CAP default 50
28
- * LINKEDIN_DAILY_REPLIES_CAP default 20
29
- * LINKEDIN_DAILY_CONNECTIONS_CAP default 15
30
- */
31
- import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync } from "fs";
32
- import { homedir } from "os";
33
- import { join } from "path";
34
- import chalk from "chalk";
35
- const VOYAGER_BASE = "https://www.linkedin.com/voyager/api";
36
- const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
37
- "AppleWebKit/537.36 (KHTML, like Gecko) " +
38
- "Chrome/127.0.0.0 Safari/537.36";
39
- // ── Storage paths ─────────────────────────────────────────────────────────────
40
- function stateDir() {
41
- return process.env.MELAYA_LINKEDIN_DIR || join(homedir(), ".melaya-runner", "linkedin");
42
- }
43
- function cookiesPath() {
44
- return join(stateDir(), "cookies.json");
45
- }
46
- function usagePath() {
47
- const day = new Date().toISOString().slice(0, 10);
48
- return join(stateDir(), `usage-${day}.json`);
49
- }
50
- // ── Caps ──────────────────────────────────────────────────────────────────────
51
- function envCap(name, fallback) {
52
- const raw = process.env[name];
53
- if (!raw)
54
- return fallback;
55
- const n = parseInt(raw, 10);
56
- return Number.isFinite(n) && n > 0 ? n : fallback;
57
- }
58
- const CAP_LIKES = envCap("LINKEDIN_DAILY_LIKES_CAP", 50);
59
- const CAP_REPLIES = envCap("LINKEDIN_DAILY_REPLIES_CAP", 20);
60
- const CAP_CONNECTIONS = envCap("LINKEDIN_DAILY_CONNECTIONS_CAP", 15);
61
- function readUsage() {
62
- const p = usagePath();
63
- if (!existsSync(p))
64
- return {};
65
- try {
66
- return JSON.parse(readFileSync(p, "utf-8"));
67
- }
68
- catch {
69
- return {};
70
- }
71
- }
72
- function bumpUsage(action) {
73
- mkdirSync(stateDir(), { recursive: true });
74
- const counts = readUsage();
75
- counts[action] = (counts[action] || 0) + 1;
76
- writeFileSync(usagePath(), JSON.stringify(counts));
77
- return counts[action];
78
- }
79
- export function readCookies() {
80
- const p = cookiesPath();
81
- if (!existsSync(p))
82
- return null;
83
- try {
84
- const raw = JSON.parse(readFileSync(p, "utf-8"));
85
- if (raw && typeof raw.li_at === "string" && typeof raw.JSESSIONID === "string"
86
- && raw.li_at.length > 10 && raw.JSESSIONID.length > 10) {
87
- return raw;
88
- }
89
- }
90
- catch { /* corrupt file */ }
91
- return null;
92
- }
93
- export function writeCookies(c) {
94
- mkdirSync(stateDir(), { recursive: true });
95
- const p = cookiesPath();
96
- writeFileSync(p, JSON.stringify(c, null, 2), "utf-8");
97
- // Lock down on POSIX. On Windows the user's profile dir ACL already
98
- // restricts read access to the user.
99
- try {
100
- chmodSync(p, 0o600);
101
- }
102
- catch { /* ignore Windows */ }
103
- return p;
104
- }
105
- function csrfToken(jsessionid) {
106
- // Voyager expects the JSESSIONID value with quotes stripped.
107
- return jsessionid.replace(/^"+|"+$/g, "");
108
- }
109
- // ── Action classification (for cap + pause) ──────────────────────────────────
110
- function classifyAction(method, path) {
111
- const m = method.toUpperCase();
112
- if (m === "GET")
113
- return "read";
114
- // Best-effort classifier — the server already passes `write: true`
115
- // for actions that should be paused/capped, so this only fires when
116
- // the server didn't tag it.
117
- if (path.includes("/feed/reactions"))
118
- return "like";
119
- if (path.includes("/feed/comments"))
120
- return "reply";
121
- if (path.includes("/voyagerRelationshipsDashMemberRelationships"))
122
- return "connect";
123
- return "read";
124
- }
125
- function capForAction(action) {
126
- const u = readUsage();
127
- switch (action) {
128
- case "like": return { used: u.likes || 0, cap: CAP_LIKES };
129
- case "reply": return { used: u.replies || 0, cap: CAP_REPLIES };
130
- case "connect": return { used: u.connections || 0, cap: CAP_CONNECTIONS };
131
- default: return null;
132
- }
133
- }
134
- function bumpForAction(action) {
135
- switch (action) {
136
- case "like": return bumpUsage("likes");
137
- case "reply": return bumpUsage("replies");
138
- case "connect": return bumpUsage("connections");
139
- default: return null;
140
- }
141
- }
142
- async function humanPause() {
143
- const ms = 3000 + Math.floor(Math.random() * 6000);
144
- await new Promise((res) => setTimeout(res, ms));
145
- }
146
- export async function handleLinkedInExec(req, verbose = false) {
147
- const cookies = readCookies();
148
- if (!cookies) {
149
- return { error: "cookies_missing — run `npx @melaya/runner linkedin-setup` once on this machine" };
150
- }
151
- const action = classifyAction(req.method, req.path);
152
- const isWrite = !!req.write || action !== "read";
153
- if (isWrite && action !== "read") {
154
- const c = capForAction(action);
155
- if (c && c.used >= c.cap) {
156
- return { error: `daily_cap_reached: ${action} ${c.used}/${c.cap}. Resets 00:00 UTC.` };
157
- }
158
- await humanPause();
159
- }
160
- const csrf = csrfToken(cookies.JSESSIONID);
161
- // Build URL with query params (URLSearchParams handles array/object encoding)
162
- const url = new URL(VOYAGER_BASE + req.path);
163
- if (req.params && typeof req.params === "object") {
164
- for (const [k, v] of Object.entries(req.params)) {
165
- if (v == null)
166
- continue;
167
- url.searchParams.append(k, String(v));
168
- }
169
- }
170
- const headers = {
171
- "User-Agent": USER_AGENT,
172
- "Accept": "application/vnd.linkedin.normalized+json+2.1",
173
- "Accept-Language": "en-US,en;q=0.9",
174
- "csrf-token": csrf,
175
- "x-restli-protocol-version": "2.0.0",
176
- "x-li-lang": "en_US",
177
- "x-li-track": JSON.stringify({
178
- clientVersion: "1.13.0", mpVersion: "1.13.0", osName: "web",
179
- timezoneOffset: -8, timezone: "America/Los_Angeles",
180
- deviceFormFactor: "DESKTOP", mpName: "voyager-web",
181
- }),
182
- "Cookie": `li_at=${cookies.li_at}; JSESSIONID=${cookies.JSESSIONID}`,
183
- "Referer": "https://www.linkedin.com/feed/",
184
- "Origin": "https://www.linkedin.com",
185
- };
186
- const init = { method: req.method, headers };
187
- if (req.body !== null && req.body !== undefined) {
188
- headers["Content-Type"] = "application/json";
189
- init.body = JSON.stringify(req.body);
190
- }
191
- if (verbose) {
192
- process.stdout.write(chalk.gray(` [linkedin] ${req.method} ${req.path}${isWrite ? " (write)" : ""}\n`));
193
- }
194
- let res;
195
- try {
196
- res = await fetch(url.toString(), init);
197
- }
198
- catch (e) {
199
- return { error: `network_error: ${e?.message || e}` };
200
- }
201
- const txt = await res.text().catch(() => "");
202
- let body = txt;
203
- try {
204
- body = JSON.parse(txt);
205
- }
206
- catch { /* keep as text */ }
207
- // Bump the counter ONLY on a 2xx — failures don't count toward the
208
- // daily quota (LinkedIn doesn't count them either).
209
- if (isWrite && res.ok) {
210
- bumpForAction(action);
211
- }
212
- if (verbose) {
213
- process.stdout.write(chalk.gray(` [linkedin] → ${res.status}\n`));
214
- }
215
- return { status: res.status, body };
216
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * `npx @melaya/runner linkedin-setup` — interactive cookie onboarding.
3
- *
4
- * Walks the operator through extracting their LinkedIn session cookies
5
- * from a logged-in browser, saves them locally to ~/.melaya-runner/
6
- * linkedin/cookies.json, and runs a sanity check (`GET /me`) to confirm
7
- * the session is live.
8
- *
9
- * Cookies are stored ONLY on this machine — never transmitted to the
10
- * Melaya server. The agents on the cloud side reach LinkedIn through
11
- * a Socket.IO proxy that forwards each call back here for egress.
12
- */
13
- export declare function runLinkedinSetup(): Promise<void>;
@@ -1,111 +0,0 @@
1
- /**
2
- * `npx @melaya/runner linkedin-setup` — interactive cookie onboarding.
3
- *
4
- * Walks the operator through extracting their LinkedIn session cookies
5
- * from a logged-in browser, saves them locally to ~/.melaya-runner/
6
- * linkedin/cookies.json, and runs a sanity check (`GET /me`) to confirm
7
- * the session is live.
8
- *
9
- * Cookies are stored ONLY on this machine — never transmitted to the
10
- * Melaya server. The agents on the cloud side reach LinkedIn through
11
- * a Socket.IO proxy that forwards each call back here for egress.
12
- */
13
- import chalk from "chalk";
14
- import { createInterface } from "readline";
15
- import { writeCookies, handleLinkedInExec } from "./linkedinProxy.js";
16
- function ask(prompt, hidden = false) {
17
- return new Promise((resolve) => {
18
- const rl = createInterface({
19
- input: process.stdin,
20
- output: process.stdout,
21
- terminal: !hidden,
22
- });
23
- if (hidden) {
24
- // Mask while typing — best-effort; works in most TTYs. Falls back
25
- // to plain echo if stdin isn't a TTY.
26
- const stdout = process.stdout;
27
- const stdin = process.stdin;
28
- if (stdin.isTTY) {
29
- const onKey = (char) => {
30
- const c = char.toString();
31
- if (c === "\r" || c === "\n")
32
- return;
33
- stdout.write("*");
34
- };
35
- stdin.on("data", onKey);
36
- rl.question(prompt, (answer) => {
37
- stdin.off("data", onKey);
38
- process.stdout.write("\n");
39
- rl.close();
40
- resolve(answer);
41
- });
42
- }
43
- else {
44
- rl.question(prompt, (answer) => { rl.close(); resolve(answer); });
45
- }
46
- }
47
- else {
48
- rl.question(prompt, (answer) => { rl.close(); resolve(answer); });
49
- }
50
- });
51
- }
52
- const ORANGE = chalk.hex("#FF8C00");
53
- const CYAN = chalk.hex("#22D3EE");
54
- const DIM = chalk.gray;
55
- export async function runLinkedinSetup() {
56
- console.log();
57
- console.log(CYAN.bold(" ▸ LinkedIn — cookie setup"));
58
- console.log(DIM(" ─────────────────────────────────────────────────────"));
59
- console.log();
60
- console.log(DIM(" Why: agents in cloud pipelines need to reach LinkedIn through"));
61
- console.log(DIM(" this machine's residential IP (datacenter IPs get flagged)."));
62
- console.log(DIM(" Cookies are saved locally only — never sent to the server."));
63
- console.log();
64
- console.log(chalk.bold(" Steps:"));
65
- console.log(` 1. Open ${ORANGE("https://www.linkedin.com")} in a logged-in browser tab`);
66
- console.log(` 2. Open DevTools ${DIM("(F12 / Cmd-Opt-I)")} → Application → Cookies → linkedin.com`);
67
- console.log(` 3. Copy the value of ${ORANGE("li_at")} (long base64-ish token)`);
68
- console.log(` 4. Copy the value of ${ORANGE("JSESSIONID")} (keep the surrounding quotes)`);
69
- console.log();
70
- const li_at = (await ask(" li_at: ", true)).trim();
71
- if (!li_at || li_at.length < 20) {
72
- console.log(chalk.red("\n ✗ li_at looks invalid (too short). Aborting."));
73
- process.exit(1);
74
- }
75
- const JSESSIONID = (await ask(" JSESSIONID: ", true)).trim();
76
- if (!JSESSIONID || JSESSIONID.length < 5) {
77
- console.log(chalk.red("\n ✗ JSESSIONID looks invalid. Aborting."));
78
- process.exit(1);
79
- }
80
- const path = writeCookies({ li_at, JSESSIONID });
81
- console.log(chalk.green(`\n ✓ Cookies saved to ${path}`));
82
- // Sanity check: hit /me to confirm the session works.
83
- console.log(DIM(" Verifying session…"));
84
- const result = await handleLinkedInExec({
85
- correlation_id: "setup-probe",
86
- method: "GET",
87
- path: "/me",
88
- });
89
- if (result.error) {
90
- console.log(chalk.red(` ✗ Verification failed: ${result.error}`));
91
- console.log(DIM(" Cookies were saved but the test call failed. Try re-pasting from a fresh browser session."));
92
- process.exit(2);
93
- }
94
- if (result.status && result.status >= 200 && result.status < 300) {
95
- const body = result.body;
96
- const name = body?.miniProfile?.firstName + " " + body?.miniProfile?.lastName
97
- || body?.firstName + " " + body?.lastName
98
- || "(name not in response)";
99
- console.log(chalk.green(` ✓ Connected as: ${name}`));
100
- console.log();
101
- console.log(DIM(" You can now run any LinkedIn pipeline from the platform."));
102
- console.log(DIM(" Start the runner normally with:"));
103
- console.log(` ${chalk.bold("npx @melaya/runner --token=mel_run_…")}`);
104
- console.log();
105
- }
106
- else {
107
- console.log(chalk.yellow(` ⚠ Unexpected status ${result.status} during verification`));
108
- console.log(DIM(` Body preview: ${JSON.stringify(result.body).slice(0, 200)}`));
109
- console.log(DIM(" Cookies were saved; try a pipeline run to see if it works."));
110
- }
111
- }