@neurowire/cli 0.4.0 → 0.5.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.
Files changed (2) hide show
  1. package/dist/index.js +91 -4
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  parseDuration,
14
14
  resolveWindow,
15
15
  selectEntries,
16
- serialize,
16
+ serialize as serialize2,
17
17
  validateNwf
18
18
  } from "@neurowire/core";
19
19
  import {
@@ -24,7 +24,77 @@ import {
24
24
  proposeTemplate
25
25
  } from "@neurowire/ingest";
26
26
  import { registerAllTaps } from "@neurowire/taps";
27
- var VERSION = "0.4.0";
27
+
28
+ // src/sinks.ts
29
+ import { serialize } from "@neurowire/core";
30
+ function sinkKind(url) {
31
+ if (url.includes("slack.com")) return "slack";
32
+ if (url.includes("discord.com") || url.includes("discordapp.com")) return "discord";
33
+ return "webhook";
34
+ }
35
+ function buildText(feedTitle, entries, max = 10) {
36
+ const lines = [`${feedTitle}: ${entries.length} new`];
37
+ for (const entry of entries.slice(0, max)) {
38
+ lines.push(`\u2022 ${entry.title} - ${entry.link}`);
39
+ }
40
+ if (entries.length > max) {
41
+ lines.push(`\u2026and ${entries.length - max} more`);
42
+ }
43
+ return lines.join("\n");
44
+ }
45
+ function buildSlackBody(feedTitle, entries) {
46
+ return { text: buildText(feedTitle, entries) };
47
+ }
48
+ function buildDiscordBody(feedTitle, entries) {
49
+ const content = buildText(feedTitle, entries);
50
+ return { content: content.length > 2e3 ? content.slice(0, 2e3) : content };
51
+ }
52
+ function buildWebhookBody(feed) {
53
+ return serialize(feed, "json");
54
+ }
55
+ var hostOf = (url) => {
56
+ try {
57
+ return new URL(url).host;
58
+ } catch {
59
+ return url;
60
+ }
61
+ };
62
+ async function deliver(url, feed) {
63
+ const kind = sinkKind(url);
64
+ let body;
65
+ let contentType;
66
+ if (kind === "slack") {
67
+ body = JSON.stringify(buildSlackBody(feed.title, feed.entries));
68
+ contentType = "application/json";
69
+ } else if (kind === "discord") {
70
+ body = JSON.stringify(buildDiscordBody(feed.title, feed.entries));
71
+ contentType = "application/json";
72
+ } else {
73
+ body = buildWebhookBody(feed);
74
+ contentType = "application/feed+json";
75
+ }
76
+ try {
77
+ const res = await fetch(url, {
78
+ method: "POST",
79
+ headers: { "content-type": contentType },
80
+ body
81
+ });
82
+ if (!res.ok) {
83
+ process.stderr.write(`[sink] ${hostOf(url)} failed: ${res.status} ${res.statusText}
84
+ `);
85
+ return false;
86
+ }
87
+ return true;
88
+ } catch (error) {
89
+ const reason = error instanceof Error ? error.message : String(error);
90
+ process.stderr.write(`[sink] ${hostOf(url)} failed: ${reason}
91
+ `);
92
+ return false;
93
+ }
94
+ }
95
+
96
+ // src/index.ts
97
+ var VERSION = "0.5.0";
28
98
  var SORT_KEYS = ["date", "title", "source"];
29
99
  var SORT_ORDERS = ["asc", "desc"];
30
100
  var FILTER_FIELDS = ["title", "summary", "source", "author", "tag"];
@@ -63,6 +133,11 @@ Watch a feed or mesh and emit only new entries:
63
133
  --interval <age> Poll interval, e.g. 30m, 6h, 1d (default: 5m).
64
134
  --state <file> JSON file of seen entry keys, so restarts skip old items.
65
135
 
136
+ Deliver to sinks (push entries to a destination):
137
+ --sink <url> POST entries to a destination. Repeatable. Slack, Discord,
138
+ or a generic webhook, auto-detected by URL. With --watch,
139
+ only the new entries are delivered each tick.
140
+
66
141
  Commands:
67
142
  validate <file-or-url> Check that an nwf document is well-formed (exits non-zero if not).
68
143
  tap doctor <url> Propose a FeedTemplate (tap) for a feed-less page.
@@ -81,6 +156,7 @@ Examples:
81
156
  neurowire --mesh ai-news.json --since 24h --sort date --format atom
82
157
  neurowire --mesh ai-news.json --filter tag:release --exclude title:sponsored --format json
83
158
  neurowire --mesh ai-news.json --watch --interval 15m --format json
159
+ neurowire --mesh ai-news.json --watch --sink https://hooks.slack.com/services/...
84
160
  neurowire validate feed.nwf
85
161
  neurowire tap doctor https://example.com/blog > ~/.config/neurowire/taps/example.com.json
86
162
  `;
@@ -327,12 +403,19 @@ function emitFeed(feed, values) {
327
403
  process.exitCode = 1;
328
404
  return false;
329
405
  }
330
- process.stdout.write(serialize(feed, values.format));
406
+ process.stdout.write(serialize2(feed, values.format));
331
407
  return true;
332
408
  }
333
409
  renderTerminal(feed);
334
410
  return true;
335
411
  }
412
+ async function deliverToSinks(feed, values) {
413
+ if (feed.entries.length === 0) return;
414
+ const sinks = values.sink ?? [];
415
+ for (const url of sinks) {
416
+ await deliver(url, feed);
417
+ }
418
+ }
336
419
  function loadSeenState(path) {
337
420
  if (!existsSync(path)) return [];
338
421
  return JSON.parse(readFileSync(path, "utf8"));
@@ -358,6 +441,7 @@ async function runWatch(values, positionals) {
358
441
  if (!refined) return;
359
442
  const fresh = newEntries(refined, seen);
360
443
  if (fresh.length > 0) emitFeed({ ...refined, entries: fresh }, values);
444
+ await deliverToSinks({ ...refined, entries: fresh }, values);
361
445
  for (const entry of fresh) seen.add(entryKey(entry));
362
446
  if (statePath) writeFileSync(statePath, JSON.stringify([...seen]));
363
447
  process.stderr.write(`[watch] ${fresh.length} new (${seen.size} seen)
@@ -390,6 +474,7 @@ async function main() {
390
474
  watch: { type: "boolean", short: "w" },
391
475
  interval: { type: "string" },
392
476
  state: { type: "string" },
477
+ sink: { type: "string", multiple: true },
393
478
  help: { type: "boolean", short: "h" },
394
479
  version: { type: "boolean", short: "v" }
395
480
  }
@@ -437,7 +522,7 @@ async function main() {
437
522
  process.exitCode = 1;
438
523
  return;
439
524
  }
440
- const output = serialize(feed, values.format);
525
+ const output = serialize2(feed, values.format);
441
526
  if (values.out) {
442
527
  writeFileSync(values.out, output);
443
528
  process.stderr.write(`Wrote ${feed.entries.length} entries to ${values.out}
@@ -445,9 +530,11 @@ async function main() {
445
530
  } else {
446
531
  process.stdout.write(output);
447
532
  }
533
+ await deliverToSinks(feed, values);
448
534
  return;
449
535
  }
450
536
  renderTerminal(feed);
537
+ await deliverToSinks(feed, values);
451
538
  }
452
539
  main().catch((error) => {
453
540
  process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neurowire/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Neurowire command-line tool: turn any blog, feed, or mesh into Atom, JSON Feed, Markdown, or nwf.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,9 +39,9 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@neurowire/core": "0.4.0",
43
- "@neurowire/ingest": "0.3.0",
44
- "@neurowire/taps": "0.2.0"
42
+ "@neurowire/ingest": "0.4.0",
43
+ "@neurowire/taps": "0.2.0",
44
+ "@neurowire/core": "0.5.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^22.10.5",