diggylab 0.1.47 → 0.1.49

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 +14 -1
  2. package/cli.js +151 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -28,6 +28,19 @@ node apps/desktop-agent/dist-cli/cli/cli.js --help
28
28
 
29
29
  State lives in `~/.diggylab` (`--config <directory>` or `DIGGYLAB_AGENT_DIR` overrides it).
30
30
 
31
+ ## Update
32
+
33
+ ```bash
34
+ diggylab update
35
+ ```
36
+
37
+ It asks npm for the newest release and upgrades the global install in place; when you are already
38
+ current it says so and changes nothing. `diggylab run` checks once a day in the background and
39
+ prints `A newer version is available: ...` when there is one, and every other command repeats the
40
+ last known notice for free — so an old worker does not go unnoticed. A systemd service needs no
41
+ reinstall — the unit points at npm's symlink, so restart it to switch versions (`systemctl restart
42
+ diggylab`).
43
+
31
44
  ## Publishing the npm package
32
45
 
33
46
  ```bash
@@ -176,7 +189,7 @@ journalctl -u diggylab -f # follow the worker log
176
189
 
177
190
  The unit keeps the same state directory and API origin you are using, restarts the worker if it
178
191
  crashes, and starts it on boot. It points at npm's `diggylab` symlink, so upgrading the package is
179
- enough — no need to reinstall the service. Run it as root (or with `sudo`) to write the unit.
192
+ enough — no need to reinstall the service (`diggylab update`, then `systemctl restart diggylab`). Run it as root (or with `sudo`) to write the unit.
180
193
 
181
194
  <details>
182
195
  <summary>Equivalent unit, if you prefer to write it yourself</summary>
package/cli.js CHANGED
@@ -180,8 +180,14 @@ function dedupeImageUrls(urls) {
180
180
  return order.map((pathname) => best.get(pathname).url).slice(0, 6);
181
181
  }
182
182
  function imageQuality(url) {
183
- const match = /[?&]stp=[^&]*?_p(\d+)x(\d+)/.exec(url);
184
- return match ? Math.max(Number(match[1]), Number(match[2])) : Number.MAX_SAFE_INTEGER;
183
+ if (/_fb30/i.test(url))
184
+ return 0;
185
+ const dimensions = [
186
+ ...url.matchAll(/(?:[?&](?:stp|cstp|ctp)=[^&]*?_s|_p)(\d+)x(\d+)/gi)
187
+ ];
188
+ if (!dimensions.length)
189
+ return Number.MAX_SAFE_INTEGER;
190
+ return Math.max(...dimensions.map((match) => Number(match[1]) * Number(match[2])));
185
191
  }
186
192
  function collectFallbackImageUrls(value, urls, depth) {
187
193
  if (!value || depth > 12 || urls.size >= 12)
@@ -423,10 +429,12 @@ function toComment(node) {
423
429
  const publishedAt = toIso(firstString(node, commentTimePaths));
424
430
  const id = firstString(node, [["id"]], false);
425
431
  const parentId = firstString(node, [["comment_direct_parent", "id"]], false);
432
+ const attachmentImages = getAttachmentImages(node);
426
433
  return {
427
434
  text: text.trim(),
428
435
  ...authorName ? { authorName } : {},
429
436
  ...authorAvatarUrl ? { authorAvatarUrl } : {},
437
+ ...attachmentImages.length ? { attachmentImages } : {},
430
438
  ...depth !== void 0 ? { depth } : {},
431
439
  ...id ? { id } : {},
432
440
  ...parentId ? { parentId } : {},
@@ -488,6 +496,12 @@ function mergeFacebookComments(items, limit) {
488
496
  existing.id = item.id;
489
497
  if (!existing.parentId && item.parentId)
490
498
  existing.parentId = item.parentId;
499
+ const attachmentImages = dedupeImageUrls([
500
+ ...existing.attachmentImages ?? [],
501
+ ...item.attachmentImages ?? []
502
+ ]);
503
+ if (attachmentImages.length)
504
+ existing.attachmentImages = attachmentImages;
491
505
  if (item.depth !== void 0) {
492
506
  existing.depth = existing.depth === void 0 ? item.depth : Math.min(existing.depth, item.depth);
493
507
  }
@@ -499,6 +513,7 @@ function mergeFacebookComments(items, limit) {
499
513
  text: text.slice(0, COMMENT_TEXT_LIMIT),
500
514
  ...authorName ? { authorName: authorName.slice(0, COMMENT_AUTHOR_LIMIT) } : {},
501
515
  ...item.authorAvatarUrl ? { authorAvatarUrl: item.authorAvatarUrl } : {},
516
+ ...item.attachmentImages?.length ? { attachmentImages: item.attachmentImages } : {},
502
517
  ...item.depth !== void 0 ? { depth: item.depth } : {},
503
518
  ...item.id ? { id: item.id } : {},
504
519
  ...item.parentId ? { parentId: item.parentId } : {},
@@ -608,10 +623,12 @@ async function extractFacebookDomComments(page) {
608
623
  };
609
624
  const texts = Array.from(article.querySelectorAll('[dir="auto"]')).filter((element) => belongsToArticle(element) && element.checkVisibility()).map((element) => readText(element)).filter(Boolean);
610
625
  const time = Array.from(article.querySelectorAll("time[datetime], abbr[data-utime]")).find((element) => belongsToArticle(element) && element.checkVisibility());
626
+ const attachmentImages = Array.from(article.querySelectorAll("img")).filter((image) => belongsToArticle(image) && image.checkVisibility()).map((image) => image.currentSrc || image.getAttribute("src") || "").filter((src) => /^https?:\/\//i.test(src) && /(scontent\.|fbcdn\.net)/i.test(src)).filter((src) => !/static\.xx\.fbcdn\.net|emoji\.php|rsrc\.php/i.test(src)).filter((src) => !/\/v\/t1\.30497-1\//i.test(src) && !/[?&]stp=cp0_/i.test(src));
611
627
  return {
612
628
  texts: Array.from(new Set(texts)),
613
629
  authorLabel: label,
614
- publishedAt: time?.getAttribute("datetime") ?? time?.getAttribute("data-utime") ?? void 0
630
+ publishedAt: time?.getAttribute("datetime") ?? time?.getAttribute("data-utime") ?? void 0,
631
+ ...attachmentImages.length ? { attachmentImages } : {}
615
632
  };
616
633
  }));
617
634
  return mergeFacebookComments(raw.flatMap((item) => {
@@ -622,9 +639,11 @@ async function extractFacebookDomComments(page) {
622
639
  if (!text)
623
640
  return [];
624
641
  const depth = parseCommentDepth(item.authorLabel);
642
+ const attachmentImages = dedupeImageUrls(item.attachmentImages ?? []);
625
643
  return [{
626
644
  text,
627
645
  ...authorName ? { authorName } : {},
646
+ ...attachmentImages.length ? { attachmentImages } : {},
628
647
  ...depth !== void 0 ? { depth } : {},
629
648
  ...toIso(item.publishedAt) ? { publishedAt: toIso(item.publishedAt) } : {}
630
649
  }];
@@ -2088,11 +2107,13 @@ function normalizeCount(value) {
2088
2107
  function normalizeComments(payload) {
2089
2108
  const comments = payload.comments.slice(0, DESKTOP_POST_COMMENTS_MAX_LIMIT).map((comment) => {
2090
2109
  const authorAvatarUrl = normalizeMediaUrl(comment.authorAvatarUrl);
2110
+ const attachmentImages = (comment.attachmentImages ?? []).map(normalizeMediaUrl).filter((url) => Boolean(url)).slice(0, 6);
2091
2111
  const depth = normalizeCount(comment.depth);
2092
2112
  return {
2093
2113
  text: comment.text.trim().slice(0, 2e3),
2094
2114
  ...comment.authorName?.trim() ? { authorName: comment.authorName.trim().slice(0, 200) } : {},
2095
2115
  ...authorAvatarUrl ? { authorAvatarUrl } : {},
2116
+ ...attachmentImages.length ? { attachmentImages } : {},
2096
2117
  ...depth !== void 0 && depth <= 5 ? { depth } : {},
2097
2118
  ...comment.publishedAt ? { publishedAt: comment.publishedAt } : {}
2098
2119
  };
@@ -3197,6 +3218,7 @@ Usage:
3197
3218
  diggylab install-service (systemd: start "run" on boot)
3198
3219
  diggylab uninstall-service
3199
3220
  diggylab status
3221
+ diggylab update (upgrades the installed npm package)
3200
3222
  diggylab install-browser
3201
3223
  diggylab unpair
3202
3224
 
@@ -3204,20 +3226,25 @@ Global options:
3204
3226
  --config <directory> Agent state directory (default ~/.diggylab)
3205
3227
  --api <origin> API origin (default $DIGGYLAB_API_URL or https://api.diggylab.com)`;
3206
3228
  var MISSING_BROWSER_HINT = "Chromium for Playwright is not installed yet. Run `diggylab install-browser` once.";
3207
- function agentVersion() {
3229
+ function readAgentManifest() {
3208
3230
  let directory = fileURLToPath(new URL(".", import.meta.url));
3209
3231
  for (let depth = 0; depth < 4; depth += 1) {
3210
3232
  const manifest = join4(directory, "package.json");
3211
3233
  if (existsSync2(manifest)) {
3212
3234
  try {
3213
- const version = JSON.parse(readFileSync2(manifest, "utf8")).version;
3214
- if (typeof version === "string" && version) return version;
3235
+ const parsed = JSON.parse(readFileSync2(manifest, "utf8"));
3236
+ if (typeof parsed.version === "string" && parsed.version) {
3237
+ return { name: typeof parsed.name === "string" ? parsed.name : "", version: parsed.version };
3238
+ }
3215
3239
  } catch {
3216
3240
  }
3217
3241
  }
3218
3242
  directory = dirname3(directory);
3219
3243
  }
3220
- return "0.0.0";
3244
+ return { name: "", version: "0.0.0" };
3245
+ }
3246
+ function agentVersion() {
3247
+ return readAgentManifest().version;
3221
3248
  }
3222
3249
  function resolvePlatform() {
3223
3250
  if (process.platform === "darwin") return "MACOS";
@@ -3570,6 +3597,7 @@ async function commandRun(runtime, io) {
3570
3597
  io.error("This agent is not paired yet. Run `diggylab pair --code <code>` first.");
3571
3598
  return 2;
3572
3599
  }
3600
+ startUpdateRefresh(runtime.directory, io);
3573
3601
  const recordActivity = (message, code, options = {}) => {
3574
3602
  void runtime.activity.append({
3575
3603
  category: options.category ?? "CONNECTION",
@@ -3727,6 +3755,119 @@ async function commandStatus(runtime, io) {
3727
3755
  io.log(`Facebook: ${hasUser ? "connected" : "missing \u2014 run `diggylab login`"}`);
3728
3756
  return 0;
3729
3757
  }
3758
+ function npmExecutable() {
3759
+ return process.platform === "win32" ? "npm.cmd" : "npm";
3760
+ }
3761
+ function runNpm(args, capture) {
3762
+ return new Promise((resolve, reject) => {
3763
+ const child = spawn(npmExecutable(), args, {
3764
+ shell: process.platform === "win32",
3765
+ stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit"
3766
+ });
3767
+ let stderr = "";
3768
+ let stdout = "";
3769
+ child.stdout?.on("data", (chunk) => {
3770
+ stdout += String(chunk);
3771
+ });
3772
+ child.stderr?.on("data", (chunk) => {
3773
+ stderr += String(chunk);
3774
+ });
3775
+ child.once("error", reject);
3776
+ child.once("exit", (value) => resolve({ code: value ?? 1, stderr, stdout }));
3777
+ });
3778
+ }
3779
+ function compareVersions(current, latest) {
3780
+ const currentParts = current.split(".").map((part) => Number.parseInt(part, 10) || 0);
3781
+ const latestParts = latest.split(".").map((part) => Number.parseInt(part, 10) || 0);
3782
+ for (let index = 0; index < 3; index += 1) {
3783
+ const difference = (currentParts[index] ?? 0) - (latestParts[index] ?? 0);
3784
+ if (difference !== 0) return difference;
3785
+ }
3786
+ return 0;
3787
+ }
3788
+ async function commandUpdate(options, io) {
3789
+ const manifest = readAgentManifest();
3790
+ if (manifest.name !== "diggylab") {
3791
+ io.error("This is a development build; update the repository instead (git pull && pnpm install).");
3792
+ return 1;
3793
+ }
3794
+ const directory = resolveConfigDirectory(options);
3795
+ let latest = "";
3796
+ try {
3797
+ const check = await runNpm(["view", "diggylab", "version", "--json"], true);
3798
+ if (check.code !== 0) {
3799
+ io.error("Could not ask npm for the latest version. Check the network and the npm registry, then try again.");
3800
+ if (check.stderr.trim()) io.error(check.stderr.trim());
3801
+ return 1;
3802
+ }
3803
+ latest = check.stdout.trim().replace(/^"(.*)"$/, "$1");
3804
+ } catch (error) {
3805
+ io.error(`Could not run npm: ${error instanceof Error ? error.message : String(error)}`);
3806
+ return 1;
3807
+ }
3808
+ if (!/^\d+\.\d+\.\d+/.test(latest)) {
3809
+ io.error(`npm returned an unexpected version "${latest.slice(0, 40)}".`);
3810
+ return 1;
3811
+ }
3812
+ writeUpdateCache(directory, latest);
3813
+ if (compareVersions(manifest.version, latest) >= 0) {
3814
+ io.log(`Already on the latest version (${manifest.version}).`);
3815
+ return 0;
3816
+ }
3817
+ io.log(`Updating diggylab ${manifest.version} -> ${latest}...`);
3818
+ try {
3819
+ const install = await runNpm(["install", "-g", "diggylab@latest"], false);
3820
+ if (install.code !== 0) {
3821
+ io.error("The npm upgrade failed. If npm reported a permissions error, fix the global prefix or run the command with sudo.");
3822
+ return 1;
3823
+ }
3824
+ } catch (error) {
3825
+ io.error(`Could not run npm: ${error instanceof Error ? error.message : String(error)}`);
3826
+ return 1;
3827
+ }
3828
+ io.log(`Updated to ${latest}. Start a new diggylab command to use it; a service picks it up on its next restart.`);
3829
+ return 0;
3830
+ }
3831
+ var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3832
+ function readUpdateCache(directory) {
3833
+ try {
3834
+ const parsed = JSON.parse(readFileSync2(join4(directory, "update-check.json"), "utf8"));
3835
+ if (typeof parsed.latest === "string" && /^\d+\.\d+\.\d+/.test(parsed.latest) && typeof parsed.checkedAt === "number") {
3836
+ return { checkedAt: parsed.checkedAt, latest: parsed.latest };
3837
+ }
3838
+ } catch {
3839
+ }
3840
+ return void 0;
3841
+ }
3842
+ function writeUpdateCache(directory, latest) {
3843
+ try {
3844
+ writeFileSync2(join4(directory, "update-check.json"), `${JSON.stringify({ checkedAt: Date.now(), latest })}
3845
+ `);
3846
+ } catch {
3847
+ }
3848
+ }
3849
+ function logUpdateNotice(io, current, latest) {
3850
+ io.log(`A newer version is available: ${current} -> ${latest} (run \`diggylab update\`).`);
3851
+ }
3852
+ function logCachedUpdateNotice(directory, io) {
3853
+ const manifest = readAgentManifest();
3854
+ if (manifest.name !== "diggylab") return;
3855
+ const cache = readUpdateCache(directory);
3856
+ if (cache && compareVersions(manifest.version, cache.latest) < 0) logUpdateNotice(io, manifest.version, cache.latest);
3857
+ }
3858
+ function startUpdateRefresh(directory, io) {
3859
+ const manifest = readAgentManifest();
3860
+ if (manifest.name !== "diggylab") return;
3861
+ const cache = readUpdateCache(directory);
3862
+ if (cache && Date.now() - cache.checkedAt < UPDATE_CHECK_INTERVAL_MS) return;
3863
+ void runNpm(["view", "diggylab", "version", "--json"], true).then((check) => {
3864
+ if (check.code !== 0) return;
3865
+ const latest = check.stdout.trim().replace(/^"(.*)"$/, "$1");
3866
+ if (!/^\d+\.\d+\.\d+/.test(latest)) return;
3867
+ writeUpdateCache(directory, latest);
3868
+ if (compareVersions(manifest.version, latest) < 0) logUpdateNotice(io, manifest.version, latest);
3869
+ }).catch(() => void 0);
3870
+ }
3730
3871
  async function commandUnpair(runtime, io) {
3731
3872
  await runtime.manager.clear();
3732
3873
  await runtime.facebook.clear();
@@ -3815,12 +3956,14 @@ async function main(argv, io = console) {
3815
3956
  io.log(USAGE);
3816
3957
  return 0;
3817
3958
  }
3818
- if (!["install-browser", "install-service", "login", "logout", "pair", "rename", "run", "status", "uninstall-service", "unpair"].includes(command)) {
3959
+ if (!["install-browser", "install-service", "login", "logout", "pair", "rename", "run", "status", "uninstall-service", "unpair", "update"].includes(command)) {
3819
3960
  io.error(`Unknown command "${command}".`);
3820
3961
  io.error(USAGE);
3821
3962
  return 2;
3822
3963
  }
3823
3964
  if (command === "install-browser") return commandInstallBrowser(io);
3965
+ if (command === "update") return commandUpdate(parsed.options, io);
3966
+ logCachedUpdateNotice(resolveConfigDirectory(parsed.options), io);
3824
3967
  const runtime = createRuntime(parsed.options);
3825
3968
  switch (command) {
3826
3969
  case "pair":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diggylab",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
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": {