diggylab 0.1.43 → 0.1.44

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 +16 -7
  2. package/cli.js +50 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -111,8 +111,12 @@ The equivalents: `diggylab login --cookies 'name=value; ...'` for a quoted one-l
111
111
  `diggylab login --cookies cookies.json` for a saved file.
112
112
 
113
113
  Only `name` and `value` are required; the agent fills in the rest (`.facebook.com`, `/`, session
114
- expiry). Every `facebook.com` cookie is kept, and at least `c_user` plus `xs` must be present — those
115
- two carry the session, and `datr` is worth keeping too.
114
+ expiry). Every `facebook.com` cookie is kept. The export must contain `c_user` (the agent rejects it
115
+ otherwise) and `xs`, which carries the session — without `xs` Facebook refuses the session during the
116
+ check, and `datr` is worth keeping too.
117
+
118
+ If you paste cookies, prefer the file form or `--cookies -` over `--cookies '...'`: a value on the
119
+ command line stays in your shell history, and pasted text stays in the terminal scrollback.
116
120
 
117
121
  Do not use `document.cookie` from the Console: Facebook's `c_user` and `xs` are HttpOnly, so they
118
122
  never appear there. The DevTools cookie table and the extension do show them.
@@ -126,12 +130,17 @@ happens the command says so — approve the login from your phone and import aga
126
130
 
127
131
  ## Troubleshooting
128
132
 
133
+ - `socket closed: 1005` followed by `session: ONLINE` about every 14 minutes is **normal**: the access
134
+ token lives 15 minutes, so the worker refreshes it one minute early and reconnects with the new one
135
+ (the disconnect lasts a second or two, and a running scan keeps going).
129
136
  - `socket closed: 4009` right after `session: ONLINE` means **two workers use the same device** — for
130
137
  example the systemd service plus a manual `diggylab run`. The worker that connected last keeps the
131
138
  device and the older one stops with a message. Check `pgrep -af "diggylab run"` and
132
139
  `systemctl status diggylab --no-pager`, then keep only one.
133
- - `socket closed: 1006` with reconnects is a dropped connection; the worker keeps retrying with
134
- backoff. If it never settles, check that the server can reach the API origin.
140
+ - `socket error:` plus `socket closed: 1006` and `RECONNECTING (NETWORK_ERROR)` is a dropped
141
+ connection; the worker retries with backoff. If it never settles, check `curl -sS -o /dev/null -w
142
+ '%{http_code}\n' https://api.diggylab.com/health` and that the machine clock is right (`date -u`),
143
+ because a clock off by more than five minutes makes the server's messages look stale.
135
144
  - `Facebook: missing` after a login means the session did not survive; run `diggylab login` again.
136
145
 
137
146
  ## Security notes
@@ -167,9 +176,9 @@ Wants=network-online.target
167
176
 
168
177
  [Service]
169
178
  ExecStart=/usr/local/bin/diggylab run
170
- Environment=DIGGYLAB_AGENT_DIR=/root/.diggylab
171
- Environment=DIGGYLAB_API_URL=https://api.diggylab.com
172
- Restart=always
179
+ Environment="DIGGYLAB_AGENT_DIR=/root/.diggylab"
180
+ Environment="DIGGYLAB_API_URL=https://api.diggylab.com"
181
+ Restart=on-failure
173
182
  RestartSec=10
174
183
 
175
184
  [Install]
package/cli.js CHANGED
@@ -2434,11 +2434,10 @@ function parseCookieTable(input) {
2434
2434
  const cookies = [];
2435
2435
  for (const line of input.split("\n")) {
2436
2436
  const columns = line.split(" ");
2437
- if (columns.length < 2) return [];
2438
- const name = columns[0].trim();
2437
+ const name = columns[0]?.trim() ?? "";
2439
2438
  const value = columns[1];
2440
- if (!name || name === "Name" || !value) continue;
2441
- cookies.push({ name, value });
2439
+ if (!value || !name || name === "Name") continue;
2440
+ cookies.push({ domain: columns[2]?.trim(), name, value });
2442
2441
  }
2443
2442
  return cookies;
2444
2443
  }
@@ -2528,6 +2527,9 @@ function createFileCrypto(keyPath) {
2528
2527
  // src/cli/systemd.ts
2529
2528
  var SERVICE_NAME = "diggylab";
2530
2529
  var SERVICE_PATH = `/etc/systemd/system/${SERVICE_NAME}.service`;
2530
+ function quote(value) {
2531
+ return /\s/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
2532
+ }
2531
2533
  function buildSystemdUnit(options) {
2532
2534
  return `[Unit]
2533
2535
  Description=DiggyLab Desktop Agent
@@ -2535,9 +2537,9 @@ After=network-online.target
2535
2537
  Wants=network-online.target
2536
2538
 
2537
2539
  [Service]
2538
- ExecStart=${options.execStart} run
2539
- Environment=DIGGYLAB_AGENT_DIR=${options.configDirectory}
2540
- Environment=DIGGYLAB_API_URL=${options.apiOrigin}
2540
+ ExecStart=${options.execStart.map(quote).join(" ")} run
2541
+ Environment="DIGGYLAB_AGENT_DIR=${options.configDirectory}"
2542
+ Environment="DIGGYLAB_API_URL=${options.apiOrigin}"
2541
2543
  Restart=on-failure
2542
2544
  RestartSec=10
2543
2545
 
@@ -2612,6 +2614,14 @@ function createRuntime(options) {
2612
2614
  function stamp() {
2613
2615
  return `[${(/* @__PURE__ */ new Date()).toTimeString().slice(0, 8)}]`;
2614
2616
  }
2617
+ function describeSocketError(error) {
2618
+ if (error instanceof Error) {
2619
+ const code = error.code;
2620
+ return `${error.message || error.name}${typeof code === "string" ? ` (${code})` : ""}`;
2621
+ }
2622
+ if (error && typeof error === "object" && "type" in error) return `event ${String(error.type)}`;
2623
+ return String(error);
2624
+ }
2615
2625
  function waitForFacebookLogin(login, timeoutMs) {
2616
2626
  return new Promise((resolve, reject) => {
2617
2627
  const timer = setTimeout(() => {
@@ -2711,17 +2721,22 @@ async function commandRename(runtime, options, io) {
2711
2721
  return 1;
2712
2722
  }
2713
2723
  const requested = options.name?.trim() || await promptForName(io);
2714
- const name = sanitizeDeviceName(requested);
2715
- if (!name) {
2724
+ if (!requested) {
2716
2725
  io.error('A device name is required, for example `diggylab rename --name "VPS H\xE0 N\u1ED9i"`.');
2717
2726
  return 2;
2718
2727
  }
2728
+ const name = sanitizeDeviceName(requested);
2719
2729
  try {
2720
2730
  const device = await runtime.manager.rename(name);
2721
2731
  io.log(`Renamed to "${device.name}".`);
2722
2732
  await printReadiness(runtime, io);
2723
2733
  return 0;
2724
2734
  } catch (error) {
2735
+ if (error instanceof DesktopAgentClientError && error.code === "DESKTOP_RESPONSE_INVALID") {
2736
+ io.error("The API answered the rename with something unexpected \u2014 usually an API that does not have this endpoint yet.");
2737
+ io.error(`Deploy the API, then check it with: curl -s -o /dev/null -w '%{http_code}\\n' -X PATCH https://api.diggylab.com/desktop-agent-auth/me -H 'content-type: application/json' -d '{"name":"x"}' (401 = the endpoint exists)`);
2738
+ return 1;
2739
+ }
2725
2740
  io.error(`Could not rename this device: ${error instanceof Error ? error.message : String(error)}`);
2726
2741
  return 1;
2727
2742
  }
@@ -2798,8 +2813,10 @@ async function importFacebookCookies(runtime, source, io) {
2798
2813
  io.error("Open facebook.com in your browser, make sure you are logged in, then copy the `cookie:` request header again.");
2799
2814
  return 1;
2800
2815
  }
2816
+ let saved = false;
2801
2817
  try {
2802
2818
  const accountId = await runtime.facebook.save(state);
2819
+ saved = true;
2803
2820
  io.log(`Facebook session saved for account ${accountId}. Asking Facebook to confirm it...`);
2804
2821
  const condition = await validateImportedSession(runtime, state);
2805
2822
  if (condition !== "READY") {
@@ -2809,6 +2826,7 @@ async function importFacebookCookies(runtime, source, io) {
2809
2826
  }
2810
2827
  } catch (error) {
2811
2828
  const message = error instanceof Error ? error.message : String(error);
2829
+ if (saved) await runtime.facebook.clear();
2812
2830
  if (/Executable doesn't exist|playwright install/i.test(message)) {
2813
2831
  io.error(MISSING_BROWSER_HINT);
2814
2832
  } else if (message === "FACEBOOK_SESSION_INVALID") {
@@ -2867,7 +2885,9 @@ function readAnswer(options = {}) {
2867
2885
  return;
2868
2886
  }
2869
2887
  if (idle) clearTimeout(idle);
2870
- idle = setTimeout(finish, 350);
2888
+ idle = setTimeout(() => {
2889
+ if (!options.until || options.until(value)) finish();
2890
+ }, 350);
2871
2891
  };
2872
2892
  process.stdin.setEncoding("utf8");
2873
2893
  process.stdin.on("data", handleData);
@@ -2895,10 +2915,14 @@ async function commandInstallBrowser(io) {
2895
2915
  child.once("exit", (value) => resolve(value ?? 1));
2896
2916
  });
2897
2917
  let code = 1;
2898
- for (const [index, args] of browserInstallCommands(process.platform).entries()) {
2899
- if (index > 0) io.log("Installing the system libraries failed; downloading the browser only...");
2900
- code = await run(args);
2901
- if (code === 0) break;
2918
+ try {
2919
+ for (const [index, args] of browserInstallCommands(process.platform).entries()) {
2920
+ if (index > 0) io.log("Installing the system libraries failed; downloading the browser only...");
2921
+ code = await run(args);
2922
+ if (code === 0) break;
2923
+ }
2924
+ } catch {
2925
+ code = 1;
2902
2926
  }
2903
2927
  if (code !== 0) {
2904
2928
  io.error(soft ? "Chromium download failed. Run `diggylab install-browser` later." : "Chromium download failed. Check the network and try again.");
@@ -2925,7 +2949,7 @@ async function commandRun(runtime, io) {
2925
2949
  headers: { authorization: `Bearer ${accessToken}` },
2926
2950
  maxPayload: 16 * 1024
2927
2951
  });
2928
- socket.on("error", (error) => io.log(`${stamp()} socket error: ${error.message}`));
2952
+ socket.on("error", (error) => io.log(`${stamp()} socket error: ${describeSocketError(error)}`));
2929
2953
  socket.on("close", (code, reason) => {
2930
2954
  io.log(`${stamp()} socket closed: ${code}${reason?.length ? ` (${reason.toString()})` : ""}`);
2931
2955
  });
@@ -3036,7 +3060,7 @@ function runCommand(command, args) {
3036
3060
  function resolveServiceExecStart() {
3037
3061
  const candidates = [join3(dirname3(process.execPath), "diggylab"), "/usr/local/bin/diggylab", "/usr/bin/diggylab"];
3038
3062
  const link = candidates.find((candidate) => existsSync2(candidate));
3039
- return link ?? `${process.execPath} ${fileURLToPath(import.meta.url)}`;
3063
+ return link ? [link] : [process.execPath, fileURLToPath(import.meta.url)];
3040
3064
  }
3041
3065
  async function commandInstallService(runtime, io) {
3042
3066
  if (process.platform !== "linux") {
@@ -3059,6 +3083,11 @@ async function commandInstallService(runtime, io) {
3059
3083
  return 1;
3060
3084
  }
3061
3085
  io.log(`Wrote ${SERVICE_PATH}.`);
3086
+ io.log(`State directory: ${runtime.directory}`);
3087
+ if (process.env.SUDO_USER && process.env.SUDO_USER !== "root" && runtime.directory === join3(homedir(), CONFIG_DIRECTORY)) {
3088
+ io.error(`Heads up: this ran as root, so the service will use ${runtime.directory} (root's home).`);
3089
+ io.error(`If you paired as "${process.env.SUDO_USER}", pass --config to point at that user's ~/${CONFIG_DIRECTORY} instead.`);
3090
+ }
3062
3091
  await runCommand("systemctl", ["daemon-reload"]).catch(() => 1);
3063
3092
  const code = await runCommand("systemctl", ["enable", "--now", SERVICE_NAME]);
3064
3093
  if (code !== 0) {
@@ -3126,8 +3155,12 @@ async function main(argv, io = console) {
3126
3155
  return commandRun(runtime, io);
3127
3156
  case "status":
3128
3157
  return commandStatus(runtime, io);
3129
- default:
3158
+ case "unpair":
3130
3159
  return commandUnpair(runtime, io);
3160
+ default:
3161
+ io.error(`Unknown command "${command}".`);
3162
+ io.error(USAGE);
3163
+ return 2;
3131
3164
  }
3132
3165
  }
3133
3166
  function isEntryPoint() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diggylab",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
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": {