@tvbs-ai/news-rd 0.1.1 → 0.2.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/cli.js +298 -66
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
var
|
|
4
|
+
var CLI_VERSION = "0.2.0";
|
|
5
|
+
var rawArgs = process.argv.slice(2);
|
|
5
6
|
function parseArgs() {
|
|
6
7
|
const options = {
|
|
7
8
|
baseUrl: process.env.RD_BASE_URL || "https://news-rundown.tvbs.ai",
|
|
@@ -11,57 +12,87 @@ function parseArgs() {
|
|
|
11
12
|
const positional = [];
|
|
12
13
|
let group = "";
|
|
13
14
|
let command = "";
|
|
14
|
-
for (let i = 0; i <
|
|
15
|
-
const arg =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (!group) {
|
|
52
|
-
group = arg;
|
|
53
|
-
} else if (!command) {
|
|
54
|
-
command = arg;
|
|
55
|
-
} else {
|
|
56
|
-
positional.push(arg);
|
|
57
|
-
}
|
|
15
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
16
|
+
const arg = rawArgs[i];
|
|
17
|
+
if (arg.startsWith("-")) {
|
|
18
|
+
switch (arg) {
|
|
19
|
+
case "--base-url":
|
|
20
|
+
options.baseUrl = rawArgs[++i] || options.baseUrl;
|
|
21
|
+
break;
|
|
22
|
+
case "--token":
|
|
23
|
+
options.token = rawArgs[++i] || "";
|
|
24
|
+
break;
|
|
25
|
+
case "--json":
|
|
26
|
+
options.json = true;
|
|
27
|
+
break;
|
|
28
|
+
case "--category":
|
|
29
|
+
options.category = rawArgs[++i];
|
|
30
|
+
break;
|
|
31
|
+
case "--content-type":
|
|
32
|
+
options.contentType = rawArgs[++i];
|
|
33
|
+
break;
|
|
34
|
+
case "--limit":
|
|
35
|
+
options.limit = rawArgs[++i];
|
|
36
|
+
break;
|
|
37
|
+
case "--version":
|
|
38
|
+
case "-v":
|
|
39
|
+
console.log(`@tvbs-ai/news-rd v${CLI_VERSION}`);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
case "--variables":
|
|
42
|
+
options.variables = rawArgs[++i];
|
|
43
|
+
break;
|
|
44
|
+
case "--help":
|
|
45
|
+
case "-h":
|
|
46
|
+
break;
|
|
47
|
+
default:
|
|
48
|
+
console.error(`Unknown option: ${arg}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
58
52
|
}
|
|
53
|
+
if (!group) {
|
|
54
|
+
group = arg;
|
|
55
|
+
} else if (!command) {
|
|
56
|
+
command = arg;
|
|
57
|
+
} else {
|
|
58
|
+
positional.push(arg);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
62
|
+
if (group) {
|
|
63
|
+
showGroupHelp(group);
|
|
64
|
+
} else {
|
|
65
|
+
showHelp();
|
|
66
|
+
}
|
|
67
|
+
process.exit(0);
|
|
59
68
|
}
|
|
60
69
|
return { group, command, positional, options };
|
|
61
70
|
}
|
|
71
|
+
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
72
|
+
var VALID_TIMESLOTS = ["0600", "0700", "0800", "1400", "1500", "1600"];
|
|
73
|
+
function requireDateTimeslot(positional, group, command) {
|
|
74
|
+
const [date, timeslot] = positional;
|
|
75
|
+
if (!date || !timeslot) {
|
|
76
|
+
console.error(`Usage: news-rd ${group} ${command} <date> <timeslot>`);
|
|
77
|
+
console.error(` date: YYYY-MM-DD (e.g. 2026-03-10)`);
|
|
78
|
+
console.error(` timeslot: ${VALID_TIMESLOTS.join(" | ")}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
if (!DATE_RE.test(date)) {
|
|
82
|
+
console.error(`Invalid date format: "${date}"`);
|
|
83
|
+
console.error(` Expected YYYY-MM-DD (e.g. 2026-03-10)`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
if (!VALID_TIMESLOTS.includes(timeslot)) {
|
|
87
|
+
console.error(`Invalid timeslot: "${timeslot}"`);
|
|
88
|
+
console.error(` Available: ${VALID_TIMESLOTS.join(", ")}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
return [date, timeslot];
|
|
92
|
+
}
|
|
62
93
|
function showHelp() {
|
|
63
94
|
console.log(`
|
|
64
|
-
tvbs-news-rd \u2014 TVBS News Rundown AI CLI
|
|
95
|
+
@tvbs-ai/news-rd v${CLI_VERSION} \u2014 TVBS News Rundown AI CLI
|
|
65
96
|
|
|
66
97
|
Usage: news-rd <group> <command> [args] [options]
|
|
67
98
|
|
|
@@ -71,11 +102,13 @@ Groups:
|
|
|
71
102
|
news \u5019\u9078\u65B0\u805E\u8207\u8DA8\u52E2\u95DC\u9375\u5B57
|
|
72
103
|
prompt Prompt \u6A21\u677F\u3001\u8B8A\u6578\u8207\u7DE8\u8B6F
|
|
73
104
|
token JWT Token \u7BA1\u7406
|
|
105
|
+
completion Shell auto-completion
|
|
74
106
|
|
|
75
107
|
Global Options:
|
|
76
108
|
--base-url URL API base URL (default: https://news-rundown.tvbs.ai)
|
|
77
109
|
--token TOKEN JWT token (or RD_TOKEN env)
|
|
78
110
|
--json Raw JSON output
|
|
111
|
+
--version, -v Show version
|
|
79
112
|
--help, -h Show help
|
|
80
113
|
|
|
81
114
|
Run 'news-rd <group> --help' for group-specific commands.
|
|
@@ -146,10 +179,25 @@ Examples:
|
|
|
146
179
|
token \u2014 JWT Token \u7BA1\u7406
|
|
147
180
|
|
|
148
181
|
Commands:
|
|
149
|
-
generate Generate a new admin JWT token
|
|
182
|
+
generate Generate a new admin JWT token
|
|
183
|
+
|
|
184
|
+
Note: Token generation requires a Clerk session cookie.
|
|
185
|
+
The easiest way is to use the Web UI \u2014 log in and click "API Token"
|
|
186
|
+
in the header. The CLI command is for scripting with an existing session.
|
|
150
187
|
|
|
151
188
|
Examples:
|
|
152
189
|
news-rd token generate
|
|
190
|
+
`,
|
|
191
|
+
completion: `
|
|
192
|
+
completion \u2014 Shell auto-completion
|
|
193
|
+
|
|
194
|
+
Commands:
|
|
195
|
+
bash Output bash completion script
|
|
196
|
+
zsh Output zsh completion script
|
|
197
|
+
|
|
198
|
+
Setup:
|
|
199
|
+
news-rd completion bash >> ~/.bashrc && source ~/.bashrc
|
|
200
|
+
news-rd completion zsh >> ~/.zshrc && source ~/.zshrc
|
|
153
201
|
`
|
|
154
202
|
};
|
|
155
203
|
function showGroupHelp(group) {
|
|
@@ -167,10 +215,70 @@ async function apiCall(path, options, method = "GET") {
|
|
|
167
215
|
if (options.token) {
|
|
168
216
|
headers["Authorization"] = `Bearer ${options.token}`;
|
|
169
217
|
}
|
|
170
|
-
|
|
218
|
+
let response;
|
|
219
|
+
try {
|
|
220
|
+
response = await fetch(url, { method, headers });
|
|
221
|
+
} catch (err) {
|
|
222
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
223
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
224
|
+
console.error(`
|
|
225
|
+
Cannot connect to ${options.baseUrl}`);
|
|
226
|
+
console.error(` Make sure the server is running, or set a different URL:
|
|
227
|
+
`);
|
|
228
|
+
console.error(` news-rd --base-url https://your-server ${path.replace(/^\//, "")}`);
|
|
229
|
+
console.error(` # or`);
|
|
230
|
+
console.error(` export RD_BASE_URL=https://your-server
|
|
231
|
+
`);
|
|
232
|
+
} else {
|
|
233
|
+
console.error(`
|
|
234
|
+
Connection error: ${msg}
|
|
235
|
+
`);
|
|
236
|
+
}
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
const contentType = response.headers.get("content-type") || "";
|
|
240
|
+
if (!contentType.includes("application/json")) {
|
|
241
|
+
if (response.status === 404) {
|
|
242
|
+
console.error(`
|
|
243
|
+
Endpoint not found: ${url}`);
|
|
244
|
+
console.error(` The server may not have the Admin API deployed yet.
|
|
245
|
+
`);
|
|
246
|
+
} else if (response.status >= 500) {
|
|
247
|
+
console.error(`
|
|
248
|
+
Server error (${response.status}) from ${options.baseUrl}`);
|
|
249
|
+
console.error(` The server may be starting up or experiencing issues.
|
|
250
|
+
`);
|
|
251
|
+
} else {
|
|
252
|
+
console.error(`
|
|
253
|
+
Unexpected response from ${options.baseUrl} (HTTP ${response.status})`);
|
|
254
|
+
console.error(` Expected JSON but received ${contentType || "unknown content type"}.`);
|
|
255
|
+
console.error(` Make sure the URL points to a News Rundown AI server.
|
|
256
|
+
`);
|
|
257
|
+
}
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
171
260
|
const data = await response.json();
|
|
172
261
|
if (!response.ok) {
|
|
262
|
+
const errorCode = data?.error?.code;
|
|
173
263
|
const errorMsg = data?.error?.message || `HTTP ${response.status}`;
|
|
264
|
+
if (response.status === 401) {
|
|
265
|
+
console.error(`
|
|
266
|
+
Authentication required.`);
|
|
267
|
+
if (errorCode === "UNAUTHORIZED") {
|
|
268
|
+
console.error(` Set your token to access the API:
|
|
269
|
+
`);
|
|
270
|
+
console.error(` news-rd --token YOUR_TOKEN ${path.replace(/^\//, "")}`);
|
|
271
|
+
console.error(` # or`);
|
|
272
|
+
console.error(` export RD_TOKEN=YOUR_TOKEN
|
|
273
|
+
`);
|
|
274
|
+
console.error(` To generate a token, log in to the web UI and click "API Token".
|
|
275
|
+
`);
|
|
276
|
+
} else {
|
|
277
|
+
console.error(` ${errorMsg}
|
|
278
|
+
`);
|
|
279
|
+
}
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
174
282
|
throw new Error(`API Error: ${errorMsg}`);
|
|
175
283
|
}
|
|
176
284
|
return data;
|
|
@@ -198,14 +306,6 @@ function printTable(rows, columns) {
|
|
|
198
306
|
console.log(cols.map((c, i) => String(row[c] ?? "").padEnd(widths[i])).join(" "));
|
|
199
307
|
}
|
|
200
308
|
}
|
|
201
|
-
function requireDateTimeslot(positional, group, command) {
|
|
202
|
-
const [date, timeslot] = positional;
|
|
203
|
-
if (!date || !timeslot) {
|
|
204
|
-
console.error(`Usage: news-rd ${group} ${command} <date> <timeslot>`);
|
|
205
|
-
process.exit(1);
|
|
206
|
-
}
|
|
207
|
-
return [date, timeslot];
|
|
208
|
-
}
|
|
209
309
|
async function handleConfig(command, _positional, options) {
|
|
210
310
|
switch (command) {
|
|
211
311
|
case "timeslots": {
|
|
@@ -214,12 +314,12 @@ async function handleConfig(command, _positional, options) {
|
|
|
214
314
|
printJson(data);
|
|
215
315
|
return;
|
|
216
316
|
}
|
|
217
|
-
printTable(data.timeslots, ["slot_code", "description"
|
|
317
|
+
printTable(data.timeslots, ["slot_code", "description"]);
|
|
218
318
|
return;
|
|
219
319
|
}
|
|
220
320
|
default:
|
|
221
321
|
showGroupHelp("config");
|
|
222
|
-
process.exit(
|
|
322
|
+
process.exit(0);
|
|
223
323
|
}
|
|
224
324
|
}
|
|
225
325
|
async function handleRundown(command, positional, options) {
|
|
@@ -231,10 +331,20 @@ async function handleRundown(command, positional, options) {
|
|
|
231
331
|
printJson(data);
|
|
232
332
|
return;
|
|
233
333
|
}
|
|
234
|
-
console.log(`Rundown: ${data.rundown_id}`);
|
|
235
334
|
const items = data.rundown.items || [];
|
|
236
|
-
console.log(`
|
|
237
|
-
|
|
335
|
+
console.log(`Rundown: ${data.rundown_id}`);
|
|
336
|
+
console.log(`Items: ${items.length}
|
|
337
|
+
`);
|
|
338
|
+
printTable(items.map((item, i) => {
|
|
339
|
+
const newsItem = item.news_item;
|
|
340
|
+
return {
|
|
341
|
+
"#": item.sequence ?? i + 1,
|
|
342
|
+
type: item.item_type,
|
|
343
|
+
title: newsItem ? String(newsItem.title ?? "").substring(0, 45) : item.placeholder?.caption ?? "-",
|
|
344
|
+
score: newsItem ? newsItem.scoring?.rundown_final_score ?? "-" : "-",
|
|
345
|
+
video_id: newsItem?.video_id ?? "-"
|
|
346
|
+
};
|
|
347
|
+
}), ["#", "type", "title", "score", "video_id"]);
|
|
238
348
|
return;
|
|
239
349
|
}
|
|
240
350
|
case "history": {
|
|
@@ -255,7 +365,7 @@ async function handleRundown(command, positional, options) {
|
|
|
255
365
|
}
|
|
256
366
|
default:
|
|
257
367
|
showGroupHelp("rundown");
|
|
258
|
-
process.exit(
|
|
368
|
+
process.exit(0);
|
|
259
369
|
}
|
|
260
370
|
}
|
|
261
371
|
async function handleNews(command, positional, options) {
|
|
@@ -312,7 +422,7 @@ ${window} keywords (${trends.keywords.length}):`);
|
|
|
312
422
|
}
|
|
313
423
|
default:
|
|
314
424
|
showGroupHelp("news");
|
|
315
|
-
process.exit(
|
|
425
|
+
process.exit(0);
|
|
316
426
|
}
|
|
317
427
|
}
|
|
318
428
|
async function handlePrompt(command, positional, options) {
|
|
@@ -399,7 +509,7 @@ ${data.compiled_prompt}
|
|
|
399
509
|
}
|
|
400
510
|
default:
|
|
401
511
|
showGroupHelp("prompt");
|
|
402
|
-
process.exit(
|
|
512
|
+
process.exit(0);
|
|
403
513
|
}
|
|
404
514
|
}
|
|
405
515
|
async function handleToken(command, _positional, options) {
|
|
@@ -418,14 +528,133 @@ Set env: export RD_TOKEN="${data.token}"`);
|
|
|
418
528
|
}
|
|
419
529
|
default:
|
|
420
530
|
showGroupHelp("token");
|
|
421
|
-
process.exit(
|
|
531
|
+
process.exit(0);
|
|
422
532
|
}
|
|
423
533
|
}
|
|
534
|
+
function handleCompletion(command) {
|
|
535
|
+
switch (command) {
|
|
536
|
+
case "bash":
|
|
537
|
+
console.log(generateBashCompletion());
|
|
538
|
+
return;
|
|
539
|
+
case "zsh":
|
|
540
|
+
console.log(generateZshCompletion());
|
|
541
|
+
return;
|
|
542
|
+
default:
|
|
543
|
+
showGroupHelp("completion");
|
|
544
|
+
process.exit(0);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function generateBashCompletion() {
|
|
548
|
+
return `# bash completion for news-rd
|
|
549
|
+
_news_rd_completions() {
|
|
550
|
+
local cur prev groups
|
|
551
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
552
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
553
|
+
groups="config rundown news prompt token completion"
|
|
554
|
+
|
|
555
|
+
case "\${COMP_WORDS[1]}" in
|
|
556
|
+
config)
|
|
557
|
+
COMPREPLY=($(compgen -W "timeslots" -- "$cur"))
|
|
558
|
+
;;
|
|
559
|
+
rundown)
|
|
560
|
+
COMPREPLY=($(compgen -W "get history" -- "$cur"))
|
|
561
|
+
;;
|
|
562
|
+
news)
|
|
563
|
+
COMPREPLY=($(compgen -W "candidates trends keywords" -- "$cur"))
|
|
564
|
+
;;
|
|
565
|
+
prompt)
|
|
566
|
+
COMPREPLY=($(compgen -W "list show variables resolve compile" -- "$cur"))
|
|
567
|
+
;;
|
|
568
|
+
token)
|
|
569
|
+
COMPREPLY=($(compgen -W "generate" -- "$cur"))
|
|
570
|
+
;;
|
|
571
|
+
completion)
|
|
572
|
+
COMPREPLY=($(compgen -W "bash zsh" -- "$cur"))
|
|
573
|
+
;;
|
|
574
|
+
*)
|
|
575
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
576
|
+
COMPREPLY=($(compgen -W "$groups --help --version --json" -- "$cur"))
|
|
577
|
+
fi
|
|
578
|
+
;;
|
|
579
|
+
esac
|
|
580
|
+
|
|
581
|
+
case "$prev" in
|
|
582
|
+
--base-url|--token|--category|--content-type|--limit|--version|--variables)
|
|
583
|
+
COMPREPLY=()
|
|
584
|
+
;;
|
|
585
|
+
get|history|candidates|trends|keywords|resolve|compile)
|
|
586
|
+
# Suggest timeslots after date
|
|
587
|
+
if [[ \${COMP_CWORD} -ge 4 ]]; then
|
|
588
|
+
COMPREPLY=($(compgen -W "0600 0700 0800 1400 1500 1600" -- "$cur"))
|
|
589
|
+
fi
|
|
590
|
+
;;
|
|
591
|
+
esac
|
|
592
|
+
}
|
|
593
|
+
complete -F _news_rd_completions news-rd`;
|
|
594
|
+
}
|
|
595
|
+
function generateZshCompletion() {
|
|
596
|
+
return `# zsh completion for news-rd
|
|
597
|
+
#compdef news-rd
|
|
598
|
+
|
|
599
|
+
_news_rd() {
|
|
600
|
+
local -a groups
|
|
601
|
+
groups=(
|
|
602
|
+
'config:\u6642\u6BB5\u8207\u74B0\u5883\u8A2D\u5B9A'
|
|
603
|
+
'rundown:Rundown \u67E5\u8A62\u8207\u7248\u672C\u6BD4\u5C0D'
|
|
604
|
+
'news:\u5019\u9078\u65B0\u805E\u8207\u8DA8\u52E2\u95DC\u9375\u5B57'
|
|
605
|
+
'prompt:Prompt \u6A21\u677F\u3001\u8B8A\u6578\u8207\u7DE8\u8B6F'
|
|
606
|
+
'token:JWT Token \u7BA1\u7406'
|
|
607
|
+
'completion:Shell auto-completion'
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
local -a timeslots
|
|
611
|
+
timeslots=(0600 0700 0800 1400 1500 1600)
|
|
612
|
+
|
|
613
|
+
_arguments -C \\
|
|
614
|
+
'--base-url[API base URL]:url:' \\
|
|
615
|
+
'--token[JWT token]:token:' \\
|
|
616
|
+
'--json[Raw JSON output]' \\
|
|
617
|
+
'--version[Show version]' \\
|
|
618
|
+
'--help[Show help]' \\
|
|
619
|
+
'1:group:->group' \\
|
|
620
|
+
'*::arg:->args'
|
|
621
|
+
|
|
622
|
+
case $state in
|
|
623
|
+
group)
|
|
624
|
+
_describe 'group' groups
|
|
625
|
+
;;
|
|
626
|
+
args)
|
|
627
|
+
case \${words[1]} in
|
|
628
|
+
config)
|
|
629
|
+
_values 'command' 'timeslots[List timeslots]'
|
|
630
|
+
;;
|
|
631
|
+
rundown)
|
|
632
|
+
_values 'command' 'get[Get rundown]' 'history[Version history]'
|
|
633
|
+
;;
|
|
634
|
+
news)
|
|
635
|
+
_values 'command' 'candidates[Scored candidates]' 'trends[Google Trends]' 'keywords[NewsRadar keywords]'
|
|
636
|
+
;;
|
|
637
|
+
prompt)
|
|
638
|
+
_values 'command' 'list[List prompts]' 'show[Get template]' 'variables[List variables]' 'resolve[Resolve variables]' 'compile[Compiled prompt]'
|
|
639
|
+
;;
|
|
640
|
+
token)
|
|
641
|
+
_values 'command' 'generate[Generate JWT token]'
|
|
642
|
+
;;
|
|
643
|
+
completion)
|
|
644
|
+
_values 'command' 'bash[Bash completion]' 'zsh[Zsh completion]'
|
|
645
|
+
;;
|
|
646
|
+
esac
|
|
647
|
+
;;
|
|
648
|
+
esac
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
_news_rd "$@"`;
|
|
652
|
+
}
|
|
424
653
|
async function main() {
|
|
425
654
|
const { group, command, positional, options } = parseArgs();
|
|
426
655
|
if (!group) {
|
|
427
656
|
showHelp();
|
|
428
|
-
process.exit(
|
|
657
|
+
process.exit(0);
|
|
429
658
|
}
|
|
430
659
|
try {
|
|
431
660
|
switch (group) {
|
|
@@ -444,6 +673,9 @@ async function main() {
|
|
|
444
673
|
case "token":
|
|
445
674
|
await handleToken(command, positional, options);
|
|
446
675
|
break;
|
|
676
|
+
case "completion":
|
|
677
|
+
handleCompletion(command);
|
|
678
|
+
break;
|
|
447
679
|
default:
|
|
448
680
|
console.error(`Unknown group: ${group}`);
|
|
449
681
|
showHelp();
|