@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.
Files changed (2) hide show
  1. package/dist/cli.js +298 -66
  2. 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 args = process.argv.slice(2);
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 < args.length; i++) {
15
- const arg = args[i];
16
- switch (arg) {
17
- case "--base-url":
18
- options.baseUrl = args[++i] || options.baseUrl;
19
- break;
20
- case "--token":
21
- options.token = args[++i] || "";
22
- break;
23
- case "--json":
24
- options.json = true;
25
- break;
26
- case "--category":
27
- options.category = args[++i];
28
- break;
29
- case "--content-type":
30
- options.contentType = args[++i];
31
- break;
32
- case "--limit":
33
- options.limit = args[++i];
34
- break;
35
- case "--version":
36
- options.version = args[++i];
37
- break;
38
- case "--variables":
39
- options.variables = args[++i];
40
- break;
41
- case "--help":
42
- case "-h":
43
- if (group) {
44
- showGroupHelp(group);
45
- } else {
46
- showHelp();
47
- }
48
- process.exit(0);
49
- break;
50
- default:
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 (requires Clerk session)
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
- const response = await fetch(url, { method, headers });
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", "include_google_trends"]);
317
+ printTable(data.timeslots, ["slot_code", "description"]);
218
318
  return;
219
319
  }
220
320
  default:
221
321
  showGroupHelp("config");
222
- process.exit(1);
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(`Items: ${items.length}`);
237
- printJson(data);
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(1);
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(1);
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(1);
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(1);
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(1);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tvbs-ai/news-rd",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "TVBS News Rundown AI CLI — query rundowns, candidates, trends, and prompts",
5
5
  "type": "module",
6
6
  "bin": {