@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.
- package/dist/index.js +91 -4
- 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
|
-
|
|
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(
|
|
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 =
|
|
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.
|
|
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/
|
|
43
|
-
"@neurowire/
|
|
44
|
-
"@neurowire/
|
|
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",
|