diggylab 0.1.41 → 0.1.42

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.
Files changed (3) hide show
  1. package/README.md +20 -48
  2. package/cli.js +23 -385
  3. package/package.json +1 -2
package/README.md CHANGED
@@ -7,7 +7,6 @@ headless Chromium — the Electron app is not involved.
7
7
  ## Requirements
8
8
 
9
9
  - Node.js 20+
10
- - Linux packages: `xvfb` and `x11vnc` — run `diggylab install-display` (Debian/Ubuntu) or install them yourself. The Docker image below bundles them.
11
10
  - Chromium for Playwright: downloaded automatically during `npm install`; rerun `diggylab install-browser` if that download failed.
12
11
 
13
12
  ## Install
@@ -37,7 +36,7 @@ npm publish apps/desktop-agent/dist-npm --access public
37
36
  ```
38
37
 
39
38
  `cli:pack` bundles the workspace code into a single `cli.js`, so the published package only depends
40
- on `playwright`, `qrcode-terminal`, and `ws`. The packed manifest copies the version from
39
+ on `playwright` and `ws`. The packed manifest copies the version from
41
40
  `apps/desktop-agent/package.json`; use the override below when the app release has not caught up yet.
42
41
 
43
42
  ### Publishing ahead of an app release
@@ -58,60 +57,30 @@ npm publish apps/desktop-agent/dist-npm --access public
58
57
  | Command | What it does |
59
58
  | --- | --- |
60
59
  | `pair --code <code> [--name <device name>] [--api <origin>]` | Pairs the server as a device. Create the code on `/settings/desktop-agent` in the web app. |
61
- | `login` | Renders the Facebook login page on a virtual screen and prints a DiggyLab link + QR code. Open it on any phone or laptop — no ports, no tunnel — and the session is stored encrypted on the server. |
60
+ | `login` | Opens Facebook's real login page in a browser window when the machine has a screen. On a server without one it asks for `facebook.com` cookies pasted from your own browser, and the session is stored encrypted. |
62
61
  | `run` | Connects the WebSocket worker, receives assigned scans, scans headlessly, uploads results. |
63
62
  | `status` | Shows the config directory, API origin, paired device, and Facebook session state. |
64
63
  | `install-browser` | Downloads the Chromium build Playwright needs (~150 MB). It also runs during `npm install`, so you only need it if that download failed. |
65
- | `install-display` | On Debian/Ubuntu, installs `xvfb` and `x11vnc` for the login screen. |
66
64
  | `unpair` | Removes device credentials, the Facebook session, and pending scans. |
67
65
 
68
- Useful options: `--api <origin>` (default `https://api.diggylab.com`), `--web-url <origin>` (only for a
69
- self-hosted web app, or `DIGGYLAB_WEB_URL`), `--display <number>` to avoid an Xvfb collision.
66
+ Useful options: `--api <origin>` (default `https://api.diggylab.com`), `--cookies <file>` to import
67
+ cookies directly.
70
68
 
71
- ## How `login` works on a server
69
+ ## How `login` works
72
70
 
73
- A server has no screen, so the agent starts `Xvfb` (virtual screen) and `x11vnc` (shares it), then
74
- carries that screen to the customer through the API connection it already uses for scans:
71
+ Two paths, depending on the machine:
75
72
 
76
- ```
77
- Xvfb → Chromium opens Facebook's real login page → encrypted over the API → link + QR → phone logs in
78
- ```
79
-
80
- Nothing has to be opened on the server: no port, no tunnel, no firewall change. The link points at
81
- the web app (`https://diggylab.com/agent-login/<session>#<key>`), and everything is torn down when the
82
- login finishes or times out (10 minutes).
83
-
84
- ### The screen is encrypted end to end
85
-
86
- The part of the link after `#` is the decryption key, and browsers never send a fragment to a server —
87
- so the relay passes bytes it cannot read. DiggyLab stores nothing: no frames, no keystrokes, no
88
- session. The Facebook cookies stay on the customer's server, sealed with `~/.diggylab/key`.
89
-
90
- The link itself is the only way in: it is single use, expires with the login window, and the session
91
- is created per `login` run.
92
-
93
- ### When the screen does not appear
94
-
95
- `login` checks the virtual screen before printing the link: it reads x11vnc's greeting, so a broken
96
- screen stops the command with the reason instead of leaving the customer with a blank page.
97
-
98
- Each process writes a log to `~/.diggylab/logs/` (`xvfb.log`, `x11vnc.log`). The tail is printed when
99
- the screen fails or a process dies during the login, and the files stay there for later inspection.
100
-
101
- Manual checks while `login` is running:
102
-
103
- ```bash
104
- pgrep -a Xvfb; pgrep -a x11vnc # both must be running
105
- ss -ltnp | grep :5999 # x11vnc listens on loopback
106
- timeout 2 bash -c 'exec 3<>/dev/tcp/127.0.0.1/5999 && head -c 12 <&3' # expect: RFB 003.008
107
- ```
73
+ | Machine | What `login` does |
74
+ | --- | --- |
75
+ | Desktop, or Linux with a display (`$DISPLAY` set) | Opens Chromium on that display with Facebook's real login page; you log in normally. |
76
+ | Server without a screen | Prints a short prompt and asks you to paste `facebook.com` cookies exported from your own browser. |
108
77
 
109
- If the browser says the link expired, run `diggylab login` again — each run mints a new session.
78
+ Both paths save the session encrypted with `~/.diggylab/key`, and `diggylab run` then uses it for scans.
110
79
 
111
80
  ### Logging in with cookies from your own browser
112
81
 
113
- If you would rather not share a screen, log into Facebook in your own browser and import those
114
- cookies instead — no Xvfb, no link, no port:
82
+ On a machine without a screen this is the only path, and you can also pick it on a desktop with
83
+ `diggylab login cookies`:
115
84
 
116
85
  1. Get the `facebook.com` cookies, in whichever way is easiest:
117
86
  - **DevTools → Network** (fastest): open facebook.com, click any request, copy the whole
@@ -119,12 +88,15 @@ cookies instead — no Xvfb, no link, no port:
119
88
  - **DevTools → Application → Cookies → `https://www.facebook.com`**: select the table and copy the
120
89
  rows.
121
90
  - **Cookie-Editor** (Chrome, Edge, Firefox): *Export → JSON*.
122
- 2. On the server:
91
+ 2. On the server, run the friendly form and paste when asked (Enter finishes the paste):
123
92
 
124
93
  ```bash
125
- diggylab login --cookies cookies.json # or: diggylab login --cookies - and paste, then Ctrl-D
94
+ diggylab login cookies
126
95
  ```
127
96
 
97
+ The equivalents: `diggylab login --cookies 'name=value; ...'` for a quoted one-liner, or
98
+ `diggylab login --cookies cookies.json` for a saved file.
99
+
128
100
  Only `name` and `value` are required; the agent fills in the rest (`.facebook.com`, `/`, session
129
101
  expiry). Every `facebook.com` cookie is kept, and at least `c_user` plus `xs` must be present — those
130
102
  two carry the session, and `datr` is worth keeping too.
@@ -169,8 +141,8 @@ WantedBy=multi-user.target
169
141
 
170
142
  ```bash
171
143
  docker build -f apps/desktop-agent/Dockerfile.cli -t diggylab .
172
- docker run -it --rm -p 6080:6080 -v diggylab:/data diggylab pair --code ABC123
173
- docker run -it --rm -p 6080:6080 -v diggylab:/data diggylab login
144
+ docker run -it --rm -v diggylab:/data diggylab pair --code ABC123
145
+ docker run -it --rm -v diggylab:/data diggylab login cookies
174
146
  docker run -d --restart unless-stopped -v diggylab:/data diggylab run
175
147
  ```
176
148
 
package/cli.js CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  // src/cli/cli.ts
4
4
  import { randomUUID as randomUUID3 } from "node:crypto";
5
- import { spawn as spawn2 } from "node:child_process";
6
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, realpathSync } from "node:fs";
5
+ import { spawn } from "node:child_process";
6
+ import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "node:fs";
7
7
  import { hostname, homedir } from "node:os";
8
- import { dirname as dirname3, join as join4 } from "node:path";
8
+ import { dirname as dirname3, join as join3 } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
 
11
11
  // ../../packages/facebook-scanner/dist/parser.js
@@ -513,7 +513,7 @@ function withTimeout(promise, timeoutMs) {
513
513
 
514
514
  // src/cli/cli.ts
515
515
  import { chromium } from "playwright";
516
- import WebSocket2 from "ws";
516
+ import WebSocket from "ws";
517
517
 
518
518
  // src/main/activity-log.ts
519
519
  import { gzip } from "node:zlib";
@@ -2315,9 +2315,7 @@ var FLAGS = {
2315
2315
  "--code": { key: "code", type: "string" },
2316
2316
  "--config": { key: "config", type: "string" },
2317
2317
  "--cookies": { key: "cookies", type: "string" },
2318
- "--display": { key: "display", type: "number" },
2319
- "--name": { key: "name", type: "string" },
2320
- "--web-url": { key: "webUrl", type: "string" }
2318
+ "--name": { key: "name", type: "string" }
2321
2319
  };
2322
2320
  function parseCliArgs(argv) {
2323
2321
  const options = {};
@@ -2495,278 +2493,6 @@ function createFileCrypto(keyPath) {
2495
2493
  };
2496
2494
  }
2497
2495
 
2498
- // src/cli/login-screen-relay.ts
2499
- import { createConnection } from "node:net";
2500
- import WebSocket from "ws";
2501
-
2502
- // src/cli/relay-crypto.ts
2503
- import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, hkdfSync, randomBytes as randomBytes2 } from "node:crypto";
2504
- var FRAME_VERSION = 1;
2505
- var IV_BYTES2 = 12;
2506
- var KEY_BYTES2 = 32;
2507
- var TAG_BYTES2 = 16;
2508
- function createRelayKey() {
2509
- return randomBytes2(KEY_BYTES2).toString("base64url");
2510
- }
2511
- function deriveRelayKeys(key, sessionId) {
2512
- const secret = Buffer.from(key, "base64url");
2513
- const salt = Buffer.from(sessionId, "utf8");
2514
- const derive = (info) => Buffer.from(hkdfSync("sha256", secret, salt, Buffer.from(info, "utf8"), KEY_BYTES2));
2515
- return { fromAgent: derive("agent-to-browser"), toAgent: derive("browser-to-agent") };
2516
- }
2517
- function sealFrame(key, payload) {
2518
- const iv = randomBytes2(IV_BYTES2);
2519
- const cipher = createCipheriv2("aes-256-gcm", key, iv);
2520
- const sealed = Buffer.concat([cipher.update(payload), cipher.final()]);
2521
- return Buffer.concat([Buffer.from([FRAME_VERSION]), iv, sealed, cipher.getAuthTag()]);
2522
- }
2523
- function openFrame(key, frame) {
2524
- if (frame.length < 1 + IV_BYTES2 + TAG_BYTES2 || frame[0] !== FRAME_VERSION) {
2525
- throw new Error("Invalid login screen frame.");
2526
- }
2527
- const decipher = createDecipheriv2("aes-256-gcm", key, frame.subarray(1, 1 + IV_BYTES2));
2528
- decipher.setAuthTag(frame.subarray(frame.length - TAG_BYTES2));
2529
- return Buffer.concat([
2530
- decipher.update(frame.subarray(1 + IV_BYTES2, frame.length - TAG_BYTES2)),
2531
- decipher.final()
2532
- ]);
2533
- }
2534
-
2535
- // src/cli/login-screen-relay.ts
2536
- var RELAY_PATH = "/desktop-agent/login-screen";
2537
- var READY_TIMEOUT_MS = 1e4;
2538
- var DEFAULT_WEB_ORIGIN = "https://diggylab.com";
2539
- async function openLoginScreenRelay(options) {
2540
- const socket = new WebSocket(`${toWebSocketOrigin(options.apiOrigin)}${RELAY_PATH}`, {
2541
- headers: { authorization: `Bearer ${options.accessToken}` }
2542
- });
2543
- let closed = false;
2544
- let keys = null;
2545
- let vnc = null;
2546
- let resolveScreen = () => void 0;
2547
- let rejectFailure = () => void 0;
2548
- const failure = new Promise((_resolve, reject) => {
2549
- rejectFailure = reject;
2550
- });
2551
- const screenConnected = new Promise((resolve) => {
2552
- resolveScreen = resolve;
2553
- });
2554
- failure.catch(() => void 0);
2555
- const close = () => {
2556
- closed = true;
2557
- vnc?.destroy();
2558
- vnc = null;
2559
- if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) socket.close();
2560
- };
2561
- const fail = (message) => {
2562
- close();
2563
- rejectFailure(new Error(message));
2564
- };
2565
- const openScreen = () => {
2566
- if (closed || vnc) return;
2567
- const connection = createConnection({ host: "127.0.0.1", port: options.rfbPort });
2568
- vnc = connection;
2569
- connection.on("data", (chunk) => {
2570
- if (closed || !keys || socket.readyState !== WebSocket.OPEN) return;
2571
- socket.send(sealFrame(keys.fromAgent, chunk), { binary: true });
2572
- });
2573
- connection.once("error", () => {
2574
- if (vnc === connection) vnc = null;
2575
- fail("Could not reach the virtual screen.");
2576
- });
2577
- connection.once("close", () => {
2578
- if (vnc === connection) vnc = null;
2579
- });
2580
- };
2581
- socket.on("message", (data, isBinary) => {
2582
- if (isBinary) {
2583
- if (!keys || !vnc || vnc.destroyed) return;
2584
- try {
2585
- vnc.write(openFrame(keys.toAgent, Buffer.from(data)));
2586
- } catch {
2587
- fail("The login screen received a corrupted frame.");
2588
- }
2589
- return;
2590
- }
2591
- const message = parseRelayMessage(data.toString());
2592
- if (message?.type === "screen-connected") {
2593
- openScreen();
2594
- resolveScreen();
2595
- } else if (message?.type === "screen-disconnected") {
2596
- vnc?.destroy();
2597
- vnc = null;
2598
- }
2599
- });
2600
- socket.once("error", () => fail("Could not reach the DiggyLab relay."));
2601
- socket.once("close", () => {
2602
- if (!closed) fail("The DiggyLab relay closed the login screen.");
2603
- });
2604
- const ready = await waitForReady(socket);
2605
- keys = deriveRelayKeys(options.key, ready.id);
2606
- const webOrigin = (options.webUrl ?? ready.webUrl ?? DEFAULT_WEB_ORIGIN).replace(/\/+$/, "");
2607
- return {
2608
- close,
2609
- failure,
2610
- screenConnected,
2611
- url: `${webOrigin}/agent-login/${encodeURIComponent(ready.id)}#${options.key}`
2612
- };
2613
- }
2614
- function parseRelayMessage(value) {
2615
- try {
2616
- const parsed = JSON.parse(value);
2617
- if (parsed && typeof parsed === "object" && typeof parsed.type === "string") {
2618
- return parsed;
2619
- }
2620
- } catch {
2621
- }
2622
- return null;
2623
- }
2624
- function waitForReady(socket) {
2625
- return new Promise((resolve, reject) => {
2626
- const timer = setTimeout(() => reject(new Error("The DiggyLab relay did not answer in time.")), READY_TIMEOUT_MS);
2627
- timer.unref();
2628
- socket.once("message", (data, isBinary) => {
2629
- clearTimeout(timer);
2630
- if (isBinary) {
2631
- reject(new Error("The DiggyLab relay answered with an unexpected frame."));
2632
- return;
2633
- }
2634
- const message = parseRelayMessage(data.toString());
2635
- if (message?.type !== "ready") {
2636
- reject(new Error("The DiggyLab relay did not accept the login screen."));
2637
- return;
2638
- }
2639
- resolve(message);
2640
- });
2641
- socket.once("error", () => {
2642
- clearTimeout(timer);
2643
- reject(new Error("Could not reach the DiggyLab relay."));
2644
- });
2645
- socket.once("close", () => {
2646
- clearTimeout(timer);
2647
- reject(new Error("The DiggyLab relay refused the login screen."));
2648
- });
2649
- });
2650
- }
2651
- function toWebSocketOrigin(origin) {
2652
- return origin.replace(/^http/i, "ws").replace(/\/+$/, "");
2653
- }
2654
-
2655
- // src/cli/qr.ts
2656
- import qrcode from "qrcode-terminal";
2657
- function terminalQr(value) {
2658
- let output = "";
2659
- qrcode.generate(value, { small: true }, (rendered) => {
2660
- output = rendered;
2661
- });
2662
- return output;
2663
- }
2664
-
2665
- // src/cli/virtual-display.ts
2666
- import { spawn } from "node:child_process";
2667
- import { createWriteStream, mkdirSync as mkdirSync2 } from "node:fs";
2668
- import { createConnection as createConnection3 } from "node:net";
2669
- import { join as join3 } from "node:path";
2670
-
2671
- // src/cli/vnc-probe.ts
2672
- import { createConnection as createConnection2 } from "node:net";
2673
- var PROBE_TIMEOUT_MS = 4e3;
2674
- var GREETING_BYTES = 12;
2675
- var GREETING_PATTERN = /^RFB \d{3}\.\d{3}\n$/;
2676
- function probeRfb(host, port, timeoutMs = PROBE_TIMEOUT_MS) {
2677
- return new Promise((resolve) => {
2678
- const socket = createConnection2({ host, port });
2679
- const finish = (result) => {
2680
- clearTimeout(timer);
2681
- socket.destroy();
2682
- resolve(result);
2683
- };
2684
- const timer = setTimeout(() => finish(false), timeoutMs);
2685
- let received = "";
2686
- socket.on("data", (chunk) => {
2687
- received += chunk.toString("latin1");
2688
- if (received.length >= GREETING_BYTES) finish(GREETING_PATTERN.test(received.slice(0, GREETING_BYTES)));
2689
- });
2690
- socket.once("error", () => finish(false));
2691
- socket.once("close", () => finish(false));
2692
- });
2693
- }
2694
-
2695
- // src/cli/virtual-display.ts
2696
- var START_TIMEOUT_MS = 15e3;
2697
- function waitForPort(host, port, timeoutMs) {
2698
- const deadline = Date.now() + timeoutMs;
2699
- return new Promise((resolve, reject) => {
2700
- const attempt = () => {
2701
- const socket = createConnection3({ host, port });
2702
- socket.once("connect", () => {
2703
- socket.destroy();
2704
- resolve();
2705
- });
2706
- socket.once("error", () => {
2707
- socket.destroy();
2708
- if (Date.now() >= deadline) {
2709
- reject(new Error(`Timed out waiting for ${host}:${port}.`));
2710
- return;
2711
- }
2712
- setTimeout(attempt, 250).unref();
2713
- });
2714
- };
2715
- attempt();
2716
- });
2717
- }
2718
- function start(name, command, args, env, deps, logDirectory) {
2719
- const child = deps.spawnProcess(command, args, env);
2720
- const log = createWriteStream(join3(logDirectory, `${name}.log`), { mode: 384 });
2721
- const failure = new Promise((_resolve, reject) => {
2722
- child.on("error", (error) => reject(error instanceof Error ? error : new Error(String(error))));
2723
- child.on("exit", (code) => reject(new Error(`${command} exited with code ${String(code)}.`)));
2724
- });
2725
- log.on("error", () => void 0);
2726
- log.write(`# ${command} ${args.join(" ")}
2727
- `);
2728
- child.stdout?.pipe?.(log, { end: false });
2729
- child.stderr?.pipe?.(log, { end: false });
2730
- return { child, failure };
2731
- }
2732
- async function startVirtualDisplay(options, overrides = {}) {
2733
- const deps = {
2734
- probeRfb,
2735
- spawnProcess: spawn,
2736
- waitForPort,
2737
- ...overrides
2738
- };
2739
- const displayNumber = options.displayNumber ?? 99;
2740
- const display = `:${displayNumber}`;
2741
- const height = options.height ?? 900;
2742
- const rfbPort = 5900 + displayNumber;
2743
- mkdirSync2(options.logDirectory, { mode: 448, recursive: true });
2744
- const xvfb = start("xvfb", "Xvfb", [display, "-screen", "0", `${options.width ?? 1280}x${height}x24`, "-nolisten", "tcp"], process.env, deps, options.logDirectory);
2745
- const vnc = start("x11vnc", "x11vnc", ["-display", display, "-localhost", "-rfbport", String(rfbPort), "-forever", "-shared", "-nopw", "-quiet"], process.env, deps, options.logDirectory);
2746
- const failure = Promise.race([vnc.failure, xvfb.failure]);
2747
- failure.catch(() => void 0);
2748
- const stop = async () => {
2749
- for (const { child } of [vnc, xvfb]) {
2750
- child.kill("SIGTERM");
2751
- await new Promise((resolve) => setTimeout(resolve, 200));
2752
- child.kill("SIGKILL");
2753
- }
2754
- };
2755
- try {
2756
- await Promise.race([
2757
- deps.waitForPort("127.0.0.1", rfbPort, options.timeoutMs ?? START_TIMEOUT_MS),
2758
- failure
2759
- ]);
2760
- if (!await deps.probeRfb("127.0.0.1", rfbPort)) {
2761
- throw new Error(`The virtual screen did not answer on 127.0.0.1:${rfbPort}.`);
2762
- }
2763
- } catch (error) {
2764
- await stop();
2765
- throw error;
2766
- }
2767
- return { display, failure, logDirectory: options.logDirectory, rfbPort, stop };
2768
- }
2769
-
2770
2496
  // src/cli/cli.ts
2771
2497
  var LOGIN_TIMEOUT_MS = 10 * 60 * 1e3;
2772
2498
  var CONFIG_DIRECTORY = ".diggylab";
@@ -2774,24 +2500,22 @@ var USAGE = `DiggyLab Desktop Agent CLI
2774
2500
 
2775
2501
  Usage:
2776
2502
  diggylab pair --code <code> [--name <device name>] [--api <https://api.diggylab.com>]
2777
- diggylab login [--display <number>] [--web-url <https://diggylab.com>]
2778
- diggylab login cookies (asks you to paste facebook.com cookies)
2779
- diggylab login --cookies <file> (or --cookies 'name=value; ...')
2503
+ diggylab login (opens a browser window when the machine has a screen)
2504
+ diggylab login cookies (no screen: paste facebook.com cookies when asked)
2505
+ diggylab login --cookies <file> (or --cookies 'name=value; ...')
2780
2506
  diggylab run
2781
2507
  diggylab status
2782
2508
  diggylab install-browser
2783
- diggylab install-display
2784
2509
  diggylab unpair
2785
2510
 
2786
2511
  Global options:
2787
2512
  --config <directory> Agent state directory (default ~/.diggylab)
2788
2513
  --api <origin> API origin (default $DIGGYLAB_API_URL or https://api.diggylab.com)`;
2789
2514
  var MISSING_BROWSER_HINT = "Chromium for Playwright is not installed yet. Run `diggylab install-browser` once.";
2790
- var MISSING_DISPLAY_HINT = "The login screen needs Xvfb and x11vnc. Run `diggylab install-display` (Debian/Ubuntu) or use the Docker image.";
2791
2515
  function agentVersion() {
2792
2516
  let directory = fileURLToPath(new URL(".", import.meta.url));
2793
2517
  for (let depth = 0; depth < 4; depth += 1) {
2794
- const manifest = join4(directory, "package.json");
2518
+ const manifest = join3(directory, "package.json");
2795
2519
  if (existsSync2(manifest)) {
2796
2520
  try {
2797
2521
  const version = JSON.parse(readFileSync2(manifest, "utf8")).version;
@@ -2808,7 +2532,7 @@ function resolvePlatform() {
2808
2532
  return process.platform === "linux" ? "LINUX" : "WINDOWS";
2809
2533
  }
2810
2534
  function resolveConfigDirectory(options) {
2811
- return options.config?.trim() || process.env.DIGGYLAB_AGENT_DIR?.trim() || join4(homedir(), CONFIG_DIRECTORY);
2535
+ return options.config?.trim() || process.env.DIGGYLAB_AGENT_DIR?.trim() || join3(homedir(), CONFIG_DIRECTORY);
2812
2536
  }
2813
2537
  function resolveEndpoint(options) {
2814
2538
  const configured = (options.api ?? process.env.DIGGYLAB_API_URL)?.trim() || "https://api.diggylab.com";
@@ -2818,29 +2542,16 @@ function resolveEndpoint(options) {
2818
2542
  function createRuntime(options) {
2819
2543
  const directory = resolveConfigDirectory(options);
2820
2544
  const endpoint = resolveEndpoint(options);
2821
- const crypto2 = createFileCrypto(join4(directory, "key"));
2822
- const credentials = new CredentialStore(join4(directory, "credentials.json"), crypto2);
2545
+ const crypto2 = createFileCrypto(join3(directory, "key"));
2546
+ const credentials = new CredentialStore(join3(directory, "credentials.json"), crypto2);
2823
2547
  const sessions = new DeviceSessionStore(credentials);
2824
2548
  const facebook = new FacebookSessionStore(credentials);
2825
- const spool = new EncryptedScanSpool(join4(directory, "scan-spool"), crypto2);
2826
- const activity = new ActivityLog({ directory: join4(directory, "activity") });
2549
+ const spool = new EncryptedScanSpool(join3(directory, "scan-spool"), crypto2);
2550
+ const activity = new ActivityLog({ directory: join3(directory, "activity") });
2827
2551
  const http = new DesktopAgentHttpClient(endpoint.httpOrigin);
2828
2552
  const manager = new DeviceCredentialManager(sessions, http, createDeviceIdentity);
2829
2553
  return { activity, browserType: createBrowserLauncher(chromium), directory, endpoint, facebook, manager, sessions, spool };
2830
2554
  }
2831
- function printDisplayLogs(directory, io, lines = 12) {
2832
- io.error(`Login screen logs: ${directory}`);
2833
- try {
2834
- for (const name of readdirSync(directory).sort()) {
2835
- if (!name.endsWith(".log")) continue;
2836
- const tail = readFileSync2(join4(directory, name), "utf8").split("\n").filter((line) => line.trim() !== "").slice(-lines);
2837
- if (tail.length === 0) continue;
2838
- io.error(`--- ${name} ---`);
2839
- for (const line of tail) io.error(line);
2840
- }
2841
- } catch {
2842
- }
2843
- }
2844
2555
  function waitForFacebookLogin(login, timeoutMs) {
2845
2556
  return new Promise((resolve, reject) => {
2846
2557
  const timer = setTimeout(() => {
@@ -2907,6 +2618,10 @@ async function commandPair(runtime, options, io) {
2907
2618
  }
2908
2619
  async function commandLogin(runtime, options, io) {
2909
2620
  if (options.cookies !== void 0) return importFacebookCookies(runtime, options.cookies || "-", io);
2621
+ if (process.platform === "linux" && !process.env.DISPLAY) {
2622
+ io.log("This server has no screen, so the login uses cookies from your browser.");
2623
+ return importFacebookCookies(runtime, "-", io);
2624
+ }
2910
2625
  const login = new FacebookLoginService({
2911
2626
  browserType: runtime.browserType,
2912
2627
  clearTimer: (timer) => clearTimeout(timer),
@@ -2924,71 +2639,20 @@ async function commandLogin(runtime, options, io) {
2924
2639
  io.log("Facebook is already connected.");
2925
2640
  return 0;
2926
2641
  }
2927
- let display = null;
2928
- let relay = null;
2929
2642
  try {
2930
- const needsVirtualDisplay = process.platform === "linux" && !process.env.DISPLAY;
2931
- if (!needsVirtualDisplay) {
2932
- io.log(process.env.DISPLAY ? `Opening the Facebook login page on display ${process.env.DISPLAY}.` : "Opening the Facebook login page in a browser window.");
2933
- } else {
2934
- const stored = await runtime.manager.load();
2935
- if (!stored) {
2936
- io.error("This server is not paired yet. Run `diggylab pair --code <code>` first.");
2937
- return 1;
2938
- }
2939
- const access = await runtime.manager.refresh();
2940
- const webUrl = options.webUrl ?? process.env.DIGGYLAB_WEB_URL?.trim();
2941
- display = await startVirtualDisplay({
2942
- displayNumber: options.display,
2943
- logDirectory: join4(runtime.directory, "logs")
2944
- });
2945
- relay = await openLoginScreenRelay({
2946
- accessToken: access.accessToken,
2947
- apiOrigin: runtime.endpoint.httpOrigin,
2948
- key: createRelayKey(),
2949
- rfbPort: display.rfbPort,
2950
- webUrl: webUrl || void 0
2951
- });
2952
- process.env.DISPLAY = display.display;
2953
- io.log(terminalQr(relay.url));
2954
- io.log(`Open this link on your phone, or scan the QR above:
2955
- ${relay.url}`);
2956
- io.log("Log into Facebook on that screen; the agent saves the session automatically.");
2957
- void relay.screenConnected.then(() => io.log("Login screen opened in the browser."));
2958
- }
2959
- const waiting = waitForFacebookLogin(login, LOGIN_TIMEOUT_MS + 6e4);
2960
- const failures = [display?.failure, relay?.failure].filter((value) => Boolean(value));
2961
- await (failures.length > 0 ? Promise.race([waiting, ...failures]) : waiting);
2643
+ io.log(process.env.DISPLAY ? `Opening the Facebook login page on display ${process.env.DISPLAY}.` : "Opening the Facebook login page in a browser window.");
2644
+ await waitForFacebookLogin(login, LOGIN_TIMEOUT_MS + 6e4);
2962
2645
  io.log("Facebook session saved.");
2963
2646
  return 0;
2964
2647
  } catch (error) {
2965
2648
  const message = error instanceof Error ? error.message : String(error);
2966
- if (/spawn (?:Xvfb|x11vnc) ENOENT/.test(message)) {
2967
- io.error(MISSING_DISPLAY_HINT);
2968
- return 1;
2969
- }
2970
- if (/CREDENTIAL_REVOKED|not paired/i.test(message)) {
2971
- io.error("This server is not paired (or was unpaired). Run `diggylab pair --code <code>` again.");
2972
- return 1;
2973
- }
2974
- if (/NETWORK_ERROR|SERVER_UNAVAILABLE|fetch failed/i.test(message)) {
2975
- io.error(`Could not reach the API: ${message}`);
2976
- return 1;
2977
- }
2978
- if (/virtual screen|login screen|DiggyLab relay|Xvfb exited|x11vnc exited/i.test(message)) {
2979
- io.error(`The login screen failed: ${message}`);
2980
- printDisplayLogs(join4(runtime.directory, "logs"), io);
2981
- return 1;
2982
- }
2983
2649
  if (/Executable doesn't exist|playwright install/i.test(message)) {
2984
2650
  io.error(MISSING_BROWSER_HINT);
2985
2651
  return 1;
2986
2652
  }
2987
2653
  throw error;
2988
2654
  } finally {
2989
- relay?.close();
2990
2655
  await login.close();
2991
- await display?.stop();
2992
2656
  }
2993
2657
  }
2994
2658
  async function importFacebookCookies(runtime, source, io) {
@@ -3107,7 +2771,7 @@ async function commandInstallBrowser(io) {
3107
2771
  }
3108
2772
  io.log("Downloading Chromium for Playwright...");
3109
2773
  const run = (args) => new Promise((resolve, reject) => {
3110
- const child = spawn2(process.execPath, [cli, ...args], { stdio: "inherit" });
2774
+ const child = spawn(process.execPath, [cli, ...args], { stdio: "inherit" });
3111
2775
  child.once("error", reject);
3112
2776
  child.once("exit", (value) => resolve(value ?? 1));
3113
2777
  });
@@ -3124,31 +2788,6 @@ async function commandInstallBrowser(io) {
3124
2788
  io.log("Chromium installed.");
3125
2789
  return 0;
3126
2790
  }
3127
- async function commandInstallDisplay(io) {
3128
- if (process.platform !== "linux") {
3129
- io.log("The shared login screen is only needed on a Linux server; desktops open a browser window.");
3130
- return 0;
3131
- }
3132
- if (!existsSync2("/usr/bin/apt-get")) {
3133
- io.error("Automatic installation supports Debian/Ubuntu. Install xvfb and x11vnc with your package manager.");
3134
- return 1;
3135
- }
3136
- const sudo = typeof process.getuid === "function" && process.getuid() !== 0 ? ["sudo"] : [];
3137
- const run = (args) => new Promise((resolve, reject) => {
3138
- const child = spawn2(args[0], args.slice(1), { stdio: "inherit" });
3139
- child.once("error", reject);
3140
- child.once("exit", (value) => resolve(value ?? 1));
3141
- });
3142
- io.log("Installing xvfb and x11vnc...");
3143
- await run([...sudo, "apt-get", "update"]).catch(() => 1);
3144
- const code = await run([...sudo, "apt-get", "install", "-y", "xvfb", "x11vnc"]);
3145
- if (code !== 0) {
3146
- io.error("Installation failed. Run the same command with apt manually.");
3147
- return code;
3148
- }
3149
- io.log("Login screen packages installed.");
3150
- return 0;
3151
- }
3152
2791
  async function commandRun(runtime, io) {
3153
2792
  const stored = await runtime.manager.load();
3154
2793
  if (!stored) {
@@ -3163,7 +2802,7 @@ async function commandRun(runtime, io) {
3163
2802
  agentVersion: agentVersion(),
3164
2803
  clearTimer: (timer) => clearTimeout(timer),
3165
2804
  createSocket: (url, accessToken) => {
3166
- const socket = new WebSocket2(url, {
2805
+ const socket = new WebSocket(url, {
3167
2806
  headers: { authorization: `Bearer ${accessToken}` },
3168
2807
  maxPayload: 16 * 1024
3169
2808
  });
@@ -3271,13 +2910,12 @@ async function main(argv, io = console) {
3271
2910
  io.log(USAGE);
3272
2911
  return 0;
3273
2912
  }
3274
- if (!["install-browser", "install-display", "login", "pair", "run", "status", "unpair"].includes(command)) {
2913
+ if (!["install-browser", "login", "pair", "run", "status", "unpair"].includes(command)) {
3275
2914
  io.error(`Unknown command "${command}".`);
3276
2915
  io.error(USAGE);
3277
2916
  return 2;
3278
2917
  }
3279
2918
  if (command === "install-browser") return commandInstallBrowser(io);
3280
- if (command === "install-display") return commandInstallDisplay(io);
3281
2919
  const runtime = createRuntime(parsed.options);
3282
2920
  switch (command) {
3283
2921
  case "pair":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diggylab",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "description": "DiggyLab Desktop Agent collects posts and information from Facebook Groups on a paired computer. (headless CLI)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,6 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "playwright": "^1.62.0",
18
- "qrcode-terminal": "^0.12.0",
19
18
  "ws": "^8.21.3"
20
19
  },
21
20
  "scripts": {