@tvbs-ai/news-rd 0.1.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 (3) hide show
  1. package/README.md +74 -0
  2. package/dist/cli.js +457 -0
  3. package/package.json +29 -0
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # @tvbs-ai/news-rd
2
+
3
+ TVBS News Rundown AI CLI — query rundowns, news candidates, Google Trends, and Langfuse prompts.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @tvbs-ai/news-rd
9
+ # or
10
+ npx @tvbs-ai/news-rd --help
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ news-rd <group> <command> [args] [options]
17
+ ```
18
+
19
+ ### Groups
20
+
21
+ | Group | Description |
22
+ |-------|-------------|
23
+ | `config` | 時段與環境設定 |
24
+ | `rundown` | Rundown 查詢與版本比對 |
25
+ | `news` | 候選新聞與趨勢關鍵字 |
26
+ | `prompt` | Prompt 模板、變數與編譯 |
27
+ | `token` | JWT Token 管理 |
28
+
29
+ ### Examples
30
+
31
+ ```bash
32
+ # List timeslots
33
+ news-rd config timeslots
34
+
35
+ # Get rundown
36
+ news-rd rundown get 2026-03-10 0600
37
+
38
+ # Get news candidates
39
+ news-rd news candidates 2026-03-10 0600 --category 政治
40
+
41
+ # Get Google Trends
42
+ news-rd news trends 2026-03-10 0600
43
+
44
+ # List prompts
45
+ news-rd prompt list
46
+
47
+ # Get compiled prompt
48
+ news-rd prompt compile 2026-03-10 0600
49
+ ```
50
+
51
+ ### Global Options
52
+
53
+ | Option | Description | Default |
54
+ |--------|-------------|---------|
55
+ | `--base-url URL` | API base URL | `http://localhost:3000` |
56
+ | `--token TOKEN` | JWT token (or `RD_TOKEN` env) | — |
57
+ | `--json` | Raw JSON output | — |
58
+ | `--help` | Show help | — |
59
+
60
+ ### Environment Variables
61
+
62
+ | Variable | Description |
63
+ |----------|-------------|
64
+ | `RD_BASE_URL` | API base URL |
65
+ | `RD_TOKEN` | JWT authentication token |
66
+
67
+ ## Requirements
68
+
69
+ - Node.js >= 18 (uses built-in fetch)
70
+ - A running TVBS News Rundown AI server
71
+
72
+ ## License
73
+
74
+ UNLICENSED
package/dist/cli.js ADDED
@@ -0,0 +1,457 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ var args = process.argv.slice(2);
5
+ function parseArgs() {
6
+ const options = {
7
+ baseUrl: process.env.RD_BASE_URL || "http://localhost:3000",
8
+ token: process.env.RD_TOKEN || "",
9
+ json: false
10
+ };
11
+ const positional = [];
12
+ let group = "";
13
+ 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
+ }
58
+ }
59
+ }
60
+ return { group, command, positional, options };
61
+ }
62
+ function showHelp() {
63
+ console.log(`
64
+ tvbs-news-rd \u2014 TVBS News Rundown AI CLI
65
+
66
+ Usage: news-rd <group> <command> [args] [options]
67
+
68
+ Groups:
69
+ config \u6642\u6BB5\u8207\u74B0\u5883\u8A2D\u5B9A
70
+ rundown Rundown \u67E5\u8A62\u8207\u7248\u672C\u6BD4\u5C0D
71
+ news \u5019\u9078\u65B0\u805E\u8207\u8DA8\u52E2\u95DC\u9375\u5B57
72
+ prompt Prompt \u6A21\u677F\u3001\u8B8A\u6578\u8207\u7DE8\u8B6F
73
+ token JWT Token \u7BA1\u7406
74
+
75
+ Global Options:
76
+ --base-url URL API base URL (default: http://localhost:3000)
77
+ --token TOKEN JWT token (or RD_TOKEN env)
78
+ --json Raw JSON output
79
+ --help, -h Show help
80
+
81
+ Run 'news-rd <group> --help' for group-specific commands.
82
+ `);
83
+ }
84
+ var groupHelps = {
85
+ config: `
86
+ config \u2014 \u6642\u6BB5\u8207\u74B0\u5883\u8A2D\u5B9A
87
+
88
+ Commands:
89
+ timeslots List all available timeslots
90
+
91
+ Examples:
92
+ news-rd config timeslots
93
+ `,
94
+ rundown: `
95
+ rundown \u2014 Rundown \u67E5\u8A62\u8207\u7248\u672C\u6BD4\u5C0D
96
+
97
+ Commands:
98
+ get <date> <timeslot> Get rundown content
99
+ history <date> <timeslot> Get version history (initial + edits)
100
+
101
+ Examples:
102
+ news-rd rundown get 2026-03-10 0600
103
+ news-rd rundown history 2026-03-10 0600
104
+ `,
105
+ news: `
106
+ news \u2014 \u5019\u9078\u65B0\u805E\u8207\u8DA8\u52E2\u95DC\u9375\u5B57
107
+
108
+ Commands:
109
+ candidates <date> <timeslot> Get scored candidates
110
+ trends <date> <timeslot> Get Google Trends (4hr/24hr/48hr)
111
+ keywords <date> <timeslot> Get NewsRadar hot keywords
112
+
113
+ Options (candidates):
114
+ --category CAT Filter by category (\u653F\u6CBB, \u793E\u6703, \u570B\u969B, ...)
115
+ --content-type TYPE Filter by content type (feature, ...)
116
+ --limit N Result limit (default 50, max 500)
117
+
118
+ Examples:
119
+ news-rd news candidates 2026-03-10 0600 --category \u653F\u6CBB --limit 20
120
+ news-rd news trends 2026-03-10 0600
121
+ news-rd news keywords 2026-03-10 0600
122
+ `,
123
+ prompt: `
124
+ prompt \u2014 Prompt \u6A21\u677F\u3001\u8B8A\u6578\u8207\u7DE8\u8B6F
125
+
126
+ Commands:
127
+ list List all Langfuse prompts
128
+ show <name> [--version N] Get prompt template
129
+ variables List available template variables
130
+ resolve <date> <timeslot> Resolve variables to actual values
131
+ compile <date> <timeslot> Get fully compiled prompt
132
+
133
+ Options:
134
+ --version N Specific prompt version (show, compile)
135
+ --variables VARS Comma-separated variable names (resolve)
136
+
137
+ Examples:
138
+ news-rd prompt list
139
+ news-rd prompt show rundown-generation-0600
140
+ news-rd prompt show rundown-generation-0600 --version 5
141
+ news-rd prompt variables
142
+ news-rd prompt resolve 2026-03-10 0600 --variables GOOGLE_TRENDS_FOUR_HR
143
+ news-rd prompt compile 2026-03-10 0600
144
+ `,
145
+ token: `
146
+ token \u2014 JWT Token \u7BA1\u7406
147
+
148
+ Commands:
149
+ generate Generate a new admin JWT token (requires Clerk session)
150
+
151
+ Examples:
152
+ news-rd token generate
153
+ `
154
+ };
155
+ function showGroupHelp(group) {
156
+ const help = groupHelps[group];
157
+ if (help) {
158
+ console.log(help);
159
+ } else {
160
+ console.error(`Unknown group: ${group}`);
161
+ showHelp();
162
+ }
163
+ }
164
+ async function apiCall(path, options, method = "GET") {
165
+ const url = `${options.baseUrl}/api/admin${path}`;
166
+ const headers = {};
167
+ if (options.token) {
168
+ headers["Authorization"] = `Bearer ${options.token}`;
169
+ }
170
+ const response = await fetch(url, { method, headers });
171
+ const data = await response.json();
172
+ if (!response.ok) {
173
+ const errorMsg = data?.error?.message || `HTTP ${response.status}`;
174
+ throw new Error(`API Error: ${errorMsg}`);
175
+ }
176
+ return data;
177
+ }
178
+ function qs(params) {
179
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
180
+ if (entries.length === 0) return "";
181
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
182
+ }
183
+ function printJson(data) {
184
+ console.log(JSON.stringify(data, null, 2));
185
+ }
186
+ function printTable(rows, columns) {
187
+ if (rows.length === 0) {
188
+ console.log("(no results)");
189
+ return;
190
+ }
191
+ const cols = columns || Object.keys(rows[0]);
192
+ const widths = cols.map(
193
+ (col) => Math.max(col.length, ...rows.map((r) => String(r[col] ?? "").length))
194
+ );
195
+ console.log(cols.map((c, i) => c.padEnd(widths[i])).join(" "));
196
+ console.log(widths.map((w) => "-".repeat(w)).join(" "));
197
+ for (const row of rows) {
198
+ console.log(cols.map((c, i) => String(row[c] ?? "").padEnd(widths[i])).join(" "));
199
+ }
200
+ }
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
+ async function handleConfig(command, _positional, options) {
210
+ switch (command) {
211
+ case "timeslots": {
212
+ const data = await apiCall("/timeslots", options);
213
+ if (options.json) {
214
+ printJson(data);
215
+ return;
216
+ }
217
+ printTable(data.timeslots, ["slot_code", "description", "include_google_trends"]);
218
+ return;
219
+ }
220
+ default:
221
+ showGroupHelp("config");
222
+ process.exit(1);
223
+ }
224
+ }
225
+ async function handleRundown(command, positional, options) {
226
+ switch (command) {
227
+ case "get": {
228
+ const [date, timeslot] = requireDateTimeslot(positional, "rundown", "get");
229
+ const data = await apiCall(`/rundown${qs({ date, timeslot })}`, options);
230
+ if (options.json) {
231
+ printJson(data);
232
+ return;
233
+ }
234
+ console.log(`Rundown: ${data.rundown_id}`);
235
+ const items = data.rundown.items || [];
236
+ console.log(`Items: ${items.length}`);
237
+ printJson(data);
238
+ return;
239
+ }
240
+ case "history": {
241
+ const [date, timeslot] = requireDateTimeslot(positional, "rundown", "history");
242
+ const data = await apiCall(`/rundown/changes${qs({ date, timeslot })}`, options);
243
+ if (options.json) {
244
+ printJson(data);
245
+ return;
246
+ }
247
+ console.log(`Total changes: ${data.total_changes}
248
+ `);
249
+ printTable(data.versions.map((v) => ({
250
+ version: v.version,
251
+ label: v.label,
252
+ modified_at: v.modified_at || "-"
253
+ })), ["version", "label", "modified_at"]);
254
+ return;
255
+ }
256
+ default:
257
+ showGroupHelp("rundown");
258
+ process.exit(1);
259
+ }
260
+ }
261
+ async function handleNews(command, positional, options) {
262
+ switch (command) {
263
+ case "candidates": {
264
+ const [date, timeslot] = requireDateTimeslot(positional, "news", "candidates");
265
+ const data = await apiCall(`/news/candidates${qs({
266
+ date,
267
+ timeslot,
268
+ category: options.category,
269
+ content_type: options.contentType,
270
+ limit: options.limit
271
+ })}`, options);
272
+ if (options.json) {
273
+ printJson(data);
274
+ return;
275
+ }
276
+ console.log(`Statistics: total=${data.statistics.total}, with_slug=${data.statistics.with_slug}, without_slug=${data.statistics.without_slug}
277
+ `);
278
+ printTable(data.candidates.map((c) => ({
279
+ score: c.final_score?.toFixed(1),
280
+ title: String(c.title).substring(0, 50),
281
+ category: c.category,
282
+ slug: c.slug || "-",
283
+ video_id: c.video_id
284
+ })), ["score", "category", "title", "slug", "video_id"]);
285
+ return;
286
+ }
287
+ case "trends": {
288
+ const [date, timeslot] = requireDateTimeslot(positional, "news", "trends");
289
+ const data = await apiCall(`/trends${qs({ date, timeslot })}`, options);
290
+ if (options.json) {
291
+ printJson(data);
292
+ return;
293
+ }
294
+ for (const [window, trends] of Object.entries(data.google_trends)) {
295
+ console.log(`
296
+ ${window} keywords (${trends.keywords.length}):`);
297
+ console.log(` ${trends.keywords.join(", ") || "(none)"}`);
298
+ }
299
+ console.log("");
300
+ return;
301
+ }
302
+ case "keywords": {
303
+ const [date, timeslot] = requireDateTimeslot(positional, "news", "keywords");
304
+ const data = await apiCall(`/newsradar${qs({ date, timeslot })}`, options);
305
+ if (options.json) {
306
+ printJson(data);
307
+ return;
308
+ }
309
+ console.log(`Hot keywords (${data.keywords.length}):`);
310
+ console.log(` ${data.keywords.join(", ") || "(none)"}`);
311
+ return;
312
+ }
313
+ default:
314
+ showGroupHelp("news");
315
+ process.exit(1);
316
+ }
317
+ }
318
+ async function handlePrompt(command, positional, options) {
319
+ switch (command) {
320
+ case "list": {
321
+ const data = await apiCall("/prompt/list", options);
322
+ if (options.json) {
323
+ printJson(data);
324
+ return;
325
+ }
326
+ console.log(`Total: ${data.total}
327
+ `);
328
+ printTable(data.prompts, ["name", "category", "latest_version", "version_count", "labels"]);
329
+ return;
330
+ }
331
+ case "show": {
332
+ const name = positional[0];
333
+ if (!name) {
334
+ console.error("Usage: news-rd prompt show <name> [--version N]");
335
+ process.exit(1);
336
+ }
337
+ const data = await apiCall(`/prompt/template${qs({ name, version: options.version })}`, options);
338
+ if (options.json) {
339
+ printJson(data);
340
+ return;
341
+ }
342
+ console.log(`Prompt: ${data.name} (v${data.version})`);
343
+ console.log(`Labels: ${data.labels?.join(", ") || "(none)"}`);
344
+ console.log(`Length: ${data.prompt_length} chars`);
345
+ console.log(`Variables: ${data.variables_in_template.join(", ")}`);
346
+ console.log(`
347
+ --- Template ---
348
+ ${data.template}
349
+ `);
350
+ return;
351
+ }
352
+ case "variables": {
353
+ const data = await apiCall("/prompt/rundown/variables/available", options);
354
+ if (options.json) {
355
+ printJson(data);
356
+ return;
357
+ }
358
+ console.log(`Available variables (${data.total}):
359
+ `);
360
+ printTable(data.variables, ["name", "value_type", "description"]);
361
+ return;
362
+ }
363
+ case "resolve": {
364
+ const [date, timeslot] = requireDateTimeslot(positional, "prompt", "resolve");
365
+ const data = await apiCall(`/prompt/rundown/variables/resolve${qs({
366
+ date,
367
+ timeslot,
368
+ variables: options.variables
369
+ })}`, options);
370
+ if (options.json) {
371
+ printJson(data);
372
+ return;
373
+ }
374
+ for (const v of data.resolved) {
375
+ console.log(`
376
+ ${v.name} (${v.value_type}, ${v.value_length} chars${v.item_count !== void 0 ? `, ${v.item_count} items` : ""}):`);
377
+ const preview = v.value.length > 200 ? v.value.substring(0, 200) + "..." : v.value;
378
+ console.log(` ${preview}`);
379
+ }
380
+ console.log("");
381
+ return;
382
+ }
383
+ case "compile": {
384
+ const [date, timeslot] = requireDateTimeslot(positional, "prompt", "compile");
385
+ const data = await apiCall(`/prompt/rundown/compiled${qs({ date, timeslot, version: options.version })}`, options);
386
+ if (options.json) {
387
+ printJson(data);
388
+ return;
389
+ }
390
+ console.log(`Prompt: ${data.prompt_name} (v${data.prompt_version})`);
391
+ console.log(`Length: ${data.prompt_length} chars`);
392
+ console.log(`Variables resolved: ${data.variables_resolved.join(", ")}`);
393
+ console.log(`Candidates: ${data.candidates_count}`);
394
+ console.log(`
395
+ --- Compiled Prompt ---
396
+ ${data.compiled_prompt}
397
+ `);
398
+ return;
399
+ }
400
+ default:
401
+ showGroupHelp("prompt");
402
+ process.exit(1);
403
+ }
404
+ }
405
+ async function handleToken(command, _positional, options) {
406
+ switch (command) {
407
+ case "generate": {
408
+ const data = await apiCall("/token", options, "POST");
409
+ if (options.json) {
410
+ printJson(data);
411
+ return;
412
+ }
413
+ console.log(`Token: ${data.token}`);
414
+ console.log(`Expires: ${data.expires_at}`);
415
+ console.log(`
416
+ Set env: export RD_TOKEN="${data.token}"`);
417
+ return;
418
+ }
419
+ default:
420
+ showGroupHelp("token");
421
+ process.exit(1);
422
+ }
423
+ }
424
+ async function main() {
425
+ const { group, command, positional, options } = parseArgs();
426
+ if (!group) {
427
+ showHelp();
428
+ process.exit(1);
429
+ }
430
+ try {
431
+ switch (group) {
432
+ case "config":
433
+ await handleConfig(command, positional, options);
434
+ break;
435
+ case "rundown":
436
+ await handleRundown(command, positional, options);
437
+ break;
438
+ case "news":
439
+ await handleNews(command, positional, options);
440
+ break;
441
+ case "prompt":
442
+ await handlePrompt(command, positional, options);
443
+ break;
444
+ case "token":
445
+ await handleToken(command, positional, options);
446
+ break;
447
+ default:
448
+ console.error(`Unknown group: ${group}`);
449
+ showHelp();
450
+ process.exit(1);
451
+ }
452
+ } catch (error) {
453
+ console.error(error instanceof Error ? error.message : String(error));
454
+ process.exit(1);
455
+ }
456
+ }
457
+ main();
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@tvbs-ai/news-rd",
3
+ "version": "0.1.0",
4
+ "description": "TVBS News Rundown AI CLI — query rundowns, candidates, trends, and prompts",
5
+ "type": "module",
6
+ "bin": {
7
+ "news-rd": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "start": "node dist/cli.js",
15
+ "dev": "tsx src/cli.ts",
16
+ "lint": "tsc --noEmit",
17
+ "prepublishOnly": "pnpm build"
18
+ },
19
+ "keywords": ["tvbs", "news", "rundown", "ai", "cli"],
20
+ "license": "UNLICENSED",
21
+ "engines": {
22
+ "node": ">=18.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "tsup": "^8.0.0",
26
+ "tsx": "^4.0.0",
27
+ "typescript": "^5.0.0"
28
+ }
29
+ }