@signal4/s4 2.1.2 → 2.4.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/README.md +145 -17
- package/bin/s4.js +0 -0
- package/bin/signal4-mcp.js +0 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +510 -63
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +124 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +172 -5
- package/dist/client.js.map +1 -1
- package/dist/format.d.ts +23 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +455 -9
- package/dist/format.js.map +1 -1
- package/dist/handlers/ads.d.ts.map +1 -1
- package/dist/handlers/ads.js +20 -2
- package/dist/handlers/ads.js.map +1 -1
- package/dist/handlers/analyze.d.ts +1 -0
- package/dist/handlers/analyze.d.ts.map +1 -1
- package/dist/handlers/analyze.js +44 -6
- package/dist/handlers/analyze.js.map +1 -1
- package/dist/handlers/claim_detail.d.ts +4 -0
- package/dist/handlers/claim_detail.d.ts.map +1 -0
- package/dist/handlers/claim_detail.js +29 -0
- package/dist/handlers/claim_detail.js.map +1 -0
- package/dist/handlers/claims.d.ts +12 -1
- package/dist/handlers/claims.d.ts.map +1 -1
- package/dist/handlers/claims.js +190 -13
- package/dist/handlers/claims.js.map +1 -1
- package/dist/handlers/explore.d.ts.map +1 -1
- package/dist/handlers/explore.js +103 -20
- package/dist/handlers/explore.js.map +1 -1
- package/dist/handlers/registry.d.ts.map +1 -1
- package/dist/handlers/registry.js +18 -2
- package/dist/handlers/registry.js.map +1 -1
- package/dist/handlers/reports.d.ts.map +1 -1
- package/dist/handlers/reports.js +69 -10
- package/dist/handlers/reports.js.map +1 -1
- package/dist/handlers/search.d.ts.map +1 -1
- package/dist/handlers/search.js +39 -10
- package/dist/handlers/search.js.map +1 -1
- package/dist/handlers/timeline.d.ts +4 -0
- package/dist/handlers/timeline.d.ts.map +1 -0
- package/dist/handlers/timeline.js +28 -0
- package/dist/handlers/timeline.js.map +1 -0
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +251 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -1
package/dist/cli.js
CHANGED
|
@@ -9,9 +9,15 @@ import { createInterface } from "node:readline/promises";
|
|
|
9
9
|
function parseArgs(argv, specs) {
|
|
10
10
|
const flags = {};
|
|
11
11
|
const positionals = [];
|
|
12
|
-
// Apply defaults
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// Apply defaults. Flags without a declared default stay undefined —
|
|
13
|
+
// pre-filling numbers with 0 turns optional filters into active ones
|
|
14
|
+
// (political-lean-min=0 + political-lean-max=0 matched zero claims).
|
|
15
|
+
for (const s of specs) {
|
|
16
|
+
if (s.default !== undefined)
|
|
17
|
+
flags[s.name] = s.default;
|
|
18
|
+
else if (s.type === "boolean")
|
|
19
|
+
flags[s.name] = false;
|
|
20
|
+
}
|
|
15
21
|
// Build lookup: --name and -alias -> spec
|
|
16
22
|
const lookup = new Map();
|
|
17
23
|
for (const s of specs) {
|
|
@@ -32,8 +38,8 @@ function parseArgs(argv, specs) {
|
|
|
32
38
|
flags[spec.name] = spec.type === "number" ? Number(val) : (val ?? "");
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
|
-
else if (arg.startsWith("-")) {
|
|
36
|
-
|
|
41
|
+
else if (arg.startsWith("-") && arg !== "-") {
|
|
42
|
+
process.stderr.write(`Warning: unknown flag "${arg}"\n`);
|
|
37
43
|
}
|
|
38
44
|
else {
|
|
39
45
|
positionals.push(arg);
|
|
@@ -53,6 +59,9 @@ const FLAG_CHANNEL = { name: "channel", alias: "c", type: "string" };
|
|
|
53
59
|
const FLAG_LANGUAGE = { name: "language", alias: "l", type: "string" };
|
|
54
60
|
const FLAG_COUNTRY = { name: "country", type: "string" };
|
|
55
61
|
const FLAG_MODE = { name: "mode", alias: "m", type: "string", default: "semantic" };
|
|
62
|
+
const FLAG_START = { name: "start", type: "string" };
|
|
63
|
+
const FLAG_END = { name: "end", type: "string" };
|
|
64
|
+
const FLAG_KEYWORD = { name: "keyword", alias: "k", type: "string" };
|
|
56
65
|
const FLAG_SKIP_CACHE = { name: "skip-cache", type: "boolean" };
|
|
57
66
|
const FLAG_JSON = { name: "json", type: "boolean" };
|
|
58
67
|
function daysRange(days) {
|
|
@@ -60,6 +69,15 @@ function daysRange(days) {
|
|
|
60
69
|
const start = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
|
|
61
70
|
return { start_date: start, end_date: end };
|
|
62
71
|
}
|
|
72
|
+
function parseSegmentId(raw) {
|
|
73
|
+
// Quote listings display IDs as "seg:12345" — accept that form verbatim.
|
|
74
|
+
const id = Number(raw.replace(/^seg:/, ""));
|
|
75
|
+
if (!Number.isInteger(id)) {
|
|
76
|
+
print(`Error: invalid segment ID "${raw}" (expected a number or seg:<number>).`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
return id;
|
|
80
|
+
}
|
|
63
81
|
function print(text) {
|
|
64
82
|
process.stdout.write(text + "\n");
|
|
65
83
|
}
|
|
@@ -86,32 +104,41 @@ const COMMANDS = {
|
|
|
86
104
|
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_DAYS, FLAG_MODE, FLAG_LIMIT, FLAG_OFFSET, FLAG_LANGUAGE, FLAG_COUNTRY,
|
|
87
105
|
{ name: "sort", type: "string", default: "relevance" },
|
|
88
106
|
{ name: "count", type: "boolean" },
|
|
107
|
+
{ name: "full-history", type: "boolean" },
|
|
108
|
+
FLAG_KEYWORD,
|
|
89
109
|
{ name: "speaker-id", type: "number" },
|
|
90
|
-
|
|
110
|
+
FLAG_START, FLAG_END,
|
|
91
111
|
FLAG_JSON],
|
|
92
112
|
run: async (args, json) => {
|
|
93
|
-
const [
|
|
113
|
+
const [positionalTopic] = args.positionals;
|
|
94
114
|
const f = args.flags;
|
|
115
|
+
const keyword = f.keyword || undefined;
|
|
116
|
+
const topic = positionalTopic || keyword || undefined;
|
|
117
|
+
const mode = keyword ? "keyword" : f.mode;
|
|
95
118
|
const days = Number(f.days) || 30;
|
|
96
119
|
const dr = f.start ? { start_date: f.start, end_date: f.end || new Date().toISOString().slice(0, 10) } : daysRange(days);
|
|
120
|
+
const fullHistory = f["full-history"];
|
|
97
121
|
const params = {
|
|
98
|
-
topic,
|
|
122
|
+
topic, speaker: f.speaker || undefined,
|
|
99
123
|
speaker_id: Number(f["speaker-id"]) || undefined,
|
|
100
|
-
|
|
124
|
+
channel: f.channel || undefined,
|
|
101
125
|
language: f.language || undefined,
|
|
102
126
|
country: f.country || undefined,
|
|
103
127
|
days, ...dr,
|
|
104
|
-
mode
|
|
128
|
+
mode, limit: Number(f.limit), offset: Number(f.offset),
|
|
105
129
|
count_only: f.count, sort: f.sort,
|
|
130
|
+
full_history: fullHistory,
|
|
106
131
|
};
|
|
107
132
|
if (json)
|
|
108
133
|
return printJson(await client.searchQuotes({
|
|
109
|
-
topic: params.topic, speaker_name: params.
|
|
110
|
-
speaker_identity_id: params.speaker_id,
|
|
134
|
+
topic: params.topic, speaker_name: params.speaker,
|
|
135
|
+
speaker_identity_id: params.speaker_id,
|
|
136
|
+
channel_names: params.channel ? params.channel.split(",").map((s) => s.trim()).filter(Boolean) : undefined,
|
|
111
137
|
start_date: params.start_date, end_date: params.end_date,
|
|
112
138
|
language: params.language, country: params.country,
|
|
113
139
|
mode: params.mode !== "semantic" ? params.mode : undefined,
|
|
114
|
-
limit: params.count_only ? 1 :
|
|
140
|
+
limit: params.count_only ? 1 : params.limit, offset: params.offset,
|
|
141
|
+
full_history: fullHistory || undefined,
|
|
115
142
|
}));
|
|
116
143
|
await runHandler("search_quotes", params);
|
|
117
144
|
},
|
|
@@ -121,41 +148,339 @@ const COMMANDS = {
|
|
|
121
148
|
description: "Search extracted claims",
|
|
122
149
|
specs: [{ name: "entity", alias: "e", type: "string" }, FLAG_SPEAKER, FLAG_CHANNEL,
|
|
123
150
|
{ name: "category", type: "string" }, { name: "stance", type: "string" },
|
|
124
|
-
FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_MODE,
|
|
151
|
+
FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_MODE, FLAG_KEYWORD,
|
|
125
152
|
{ name: "sort", type: "string", default: "relevance" },
|
|
126
153
|
{ name: "min-obs", type: "number", default: 1 },
|
|
127
|
-
|
|
154
|
+
{ name: "min-similarity", type: "number" },
|
|
155
|
+
{ name: "political-lean-min", type: "number" }, { name: "political-lean-max", type: "number" },
|
|
156
|
+
{ name: "source", type: "string" },
|
|
157
|
+
FLAG_DAYS, FLAG_LIMIT, FLAG_OFFSET, FLAG_START, FLAG_END, FLAG_JSON],
|
|
128
158
|
run: async (args, json) => {
|
|
129
|
-
const [
|
|
159
|
+
const [positionalTopic] = args.positionals;
|
|
130
160
|
const f = args.flags;
|
|
161
|
+
const keyword = f.keyword || undefined;
|
|
162
|
+
const topic = positionalTopic || keyword || undefined;
|
|
163
|
+
const mode = keyword ? "keyword" : f.mode;
|
|
131
164
|
const days = Number(f.days) || 30;
|
|
165
|
+
const dr = f.start ? { start_date: f.start, end_date: f.end || new Date().toISOString().slice(0, 10) } : undefined;
|
|
166
|
+
const leanMin = f["political-lean-min"] !== undefined ? Number(f["political-lean-min"]) : undefined;
|
|
167
|
+
const leanMax = f["political-lean-max"] !== undefined ? Number(f["political-lean-max"]) : undefined;
|
|
168
|
+
const minSim = f["min-similarity"] !== undefined ? Number(f["min-similarity"]) : undefined;
|
|
132
169
|
const params = {
|
|
133
170
|
topic, entity: f.entity || undefined,
|
|
134
|
-
|
|
135
|
-
|
|
171
|
+
speaker: f.speaker || undefined,
|
|
172
|
+
channel: f.channel || undefined,
|
|
136
173
|
category: f.category || undefined,
|
|
137
174
|
stance: f.stance || undefined,
|
|
138
175
|
language: f.language || undefined,
|
|
139
|
-
|
|
176
|
+
country: f.country || undefined,
|
|
177
|
+
political_lean_min: leanMin, political_lean_max: leanMax,
|
|
178
|
+
min_similarity: minSim,
|
|
179
|
+
source: f.source || undefined,
|
|
180
|
+
days, mode,
|
|
140
181
|
sort: f.sort, min_observations: Number(f["min-obs"]) || 1,
|
|
141
182
|
limit: Number(f.limit), offset: Number(f.offset),
|
|
183
|
+
...dr,
|
|
142
184
|
};
|
|
185
|
+
const claimsDr = dr ?? daysRange(days);
|
|
143
186
|
if (json)
|
|
144
187
|
return printJson(await client.searchClaims({
|
|
145
|
-
topic: params.topic, entity: params.entity, speaker_name: params.
|
|
146
|
-
channel_names: params.
|
|
147
|
-
|
|
148
|
-
|
|
188
|
+
topic: params.topic, entity: params.entity, speaker_name: params.speaker,
|
|
189
|
+
channel_names: params.channel ? params.channel.split(",").map((s) => s.trim()).filter(Boolean) : undefined,
|
|
190
|
+
category: params.category, stance: params.stance, language: params.language,
|
|
191
|
+
country: params.country,
|
|
192
|
+
political_lean_min: leanMin, political_lean_max: leanMax,
|
|
193
|
+
min_similarity: minSim,
|
|
194
|
+
start_date: claimsDr.start_date, end_date: claimsDr.end_date,
|
|
195
|
+
mode: params.mode, sort: params.sort,
|
|
149
196
|
min_observations: params.min_observations > 1 ? params.min_observations : undefined,
|
|
150
|
-
limit:
|
|
197
|
+
limit: params.limit, offset: params.offset,
|
|
151
198
|
}));
|
|
152
199
|
await runHandler("search_claims", params);
|
|
153
200
|
},
|
|
154
201
|
},
|
|
202
|
+
"analyze-claims": {
|
|
203
|
+
tool: "analyze_claims",
|
|
204
|
+
description: "Analyze a topic over extracted claims — themes with observation stats (fast, no LLM)",
|
|
205
|
+
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_LANGUAGE, FLAG_DAYS,
|
|
206
|
+
{ name: "min-obs", type: "number", default: 1 }, FLAG_JSON],
|
|
207
|
+
run: async (args, json) => {
|
|
208
|
+
const [topic] = args.positionals;
|
|
209
|
+
if (!topic) {
|
|
210
|
+
print("Error: topic is required.");
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
const f = args.flags;
|
|
214
|
+
const channelArg = f.channel || undefined;
|
|
215
|
+
const params = {
|
|
216
|
+
topic,
|
|
217
|
+
speaker: f.speaker || undefined,
|
|
218
|
+
channels: channelArg?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
219
|
+
language: f.language || undefined,
|
|
220
|
+
days: Number(f.days) || undefined,
|
|
221
|
+
min_observations: Number(f["min-obs"]) || undefined,
|
|
222
|
+
};
|
|
223
|
+
if (json)
|
|
224
|
+
return printJson(await client.analyzeTopicClaims({
|
|
225
|
+
topic, speaker_name: params.speaker, channel_names: params.channels,
|
|
226
|
+
language: params.language, days: params.days, min_observations: params.min_observations,
|
|
227
|
+
}));
|
|
228
|
+
await runHandler("analyze_claims", params);
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
"trend-claims": {
|
|
232
|
+
tool: "trend_claims",
|
|
233
|
+
description: "Detect whether a topic is surging/fading by claim volume (fast)",
|
|
234
|
+
specs: [{ name: "recent-days", type: "number", default: 7 },
|
|
235
|
+
{ name: "baseline-days", type: "number", default: 30 },
|
|
236
|
+
{ name: "min-similarity", type: "number" }, FLAG_JSON],
|
|
237
|
+
run: async (args, json) => {
|
|
238
|
+
const [topic] = args.positionals;
|
|
239
|
+
if (!topic) {
|
|
240
|
+
print("Error: topic is required.");
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
const f = args.flags;
|
|
244
|
+
const params = {
|
|
245
|
+
topic,
|
|
246
|
+
recent_days: Number(f["recent-days"]) || undefined,
|
|
247
|
+
baseline_days: Number(f["baseline-days"]) || undefined,
|
|
248
|
+
min_similarity: f["min-similarity"] !== undefined ? Number(f["min-similarity"]) : undefined,
|
|
249
|
+
};
|
|
250
|
+
if (json)
|
|
251
|
+
return printJson(await client.detectTrendClaims(params));
|
|
252
|
+
await runHandler("trend_claims", params);
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
"contradict-claims": {
|
|
256
|
+
tool: "contradict_claims",
|
|
257
|
+
description: "Find a speaker's contradictory claims on a topic via stance analysis (fast)",
|
|
258
|
+
specs: [FLAG_SPEAKER, FLAG_DAYS, FLAG_JSON],
|
|
259
|
+
run: async (args, json) => {
|
|
260
|
+
const [topic] = args.positionals;
|
|
261
|
+
if (!topic) {
|
|
262
|
+
print("Error: topic is required.");
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
const f = args.flags;
|
|
266
|
+
const speaker = f.speaker || undefined;
|
|
267
|
+
if (!speaker) {
|
|
268
|
+
print("Error: --speaker is required.");
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
const params = { topic, speaker, days: Number(f.days) || undefined };
|
|
272
|
+
if (json)
|
|
273
|
+
return printJson(await client.checkContradictionsClaims({
|
|
274
|
+
topic, speaker_name: speaker, days: params.days,
|
|
275
|
+
}));
|
|
276
|
+
await runHandler("contradict_claims", params);
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
"compare-claims": {
|
|
280
|
+
tool: "compare_claims",
|
|
281
|
+
description: "Compare 2-5 speakers' claims on a topic — side-by-side stance (fast)",
|
|
282
|
+
specs: [{ name: "speakers", type: "string" }, FLAG_SPEAKER, FLAG_DAYS, FLAG_JSON],
|
|
283
|
+
run: async (args, json) => {
|
|
284
|
+
const [topic] = args.positionals;
|
|
285
|
+
if (!topic) {
|
|
286
|
+
print("Error: topic is required.");
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
const f = args.flags;
|
|
290
|
+
const speakersStr = f.speakers || f.speaker || "";
|
|
291
|
+
const speakers = speakersStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
292
|
+
if (speakers.length < 2) {
|
|
293
|
+
print("Error: --speakers requires 2-5 comma-separated names.");
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
const params = { topic, speakers, days: Number(f.days) || undefined };
|
|
297
|
+
if (json)
|
|
298
|
+
return printJson(await client.compareSpeakersClaims({
|
|
299
|
+
topic, speaker_names: speakers, days: params.days,
|
|
300
|
+
}));
|
|
301
|
+
await runHandler("compare_claims", params);
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
"channel-dossier": {
|
|
305
|
+
tool: "channel_dossier",
|
|
306
|
+
description: "Claims dossier of what a channel (show) platformed on a topic (fast)",
|
|
307
|
+
specs: [FLAG_CHANNEL, FLAG_DAYS, FLAG_JSON],
|
|
308
|
+
run: async (args, json) => {
|
|
309
|
+
const [topic] = args.positionals;
|
|
310
|
+
if (!topic) {
|
|
311
|
+
print("Error: topic is required.");
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
const f = args.flags;
|
|
315
|
+
const channel = f.channel || undefined;
|
|
316
|
+
if (!channel) {
|
|
317
|
+
print("Error: --channel is required.");
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
const channels = channel.split(",").map((s) => s.trim()).filter(Boolean);
|
|
321
|
+
const days = Number(f.days) || undefined;
|
|
322
|
+
if (json)
|
|
323
|
+
return printJson(await client.buildChannelDossier({
|
|
324
|
+
topic, channel_names: channels, days,
|
|
325
|
+
}));
|
|
326
|
+
await runHandler("channel_dossier", { topic, channels, days });
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
"compare-channels": {
|
|
330
|
+
tool: "compare_channels",
|
|
331
|
+
description: "Compare 2-5 channels' claims on a topic — stance + speaker overlap (fast)",
|
|
332
|
+
specs: [{ name: "channels", type: "string" }, FLAG_CHANNEL, FLAG_DAYS, FLAG_JSON],
|
|
333
|
+
run: async (args, json) => {
|
|
334
|
+
const [topic] = args.positionals;
|
|
335
|
+
if (!topic) {
|
|
336
|
+
print("Error: topic is required.");
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
const f = args.flags;
|
|
340
|
+
const channelsStr = f.channels || f.channel || "";
|
|
341
|
+
const channels = channelsStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
342
|
+
if (channels.length < 2) {
|
|
343
|
+
print("Error: --channels requires 2-5 comma-separated names.");
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
const days = Number(f.days) || undefined;
|
|
347
|
+
if (json)
|
|
348
|
+
return printJson(await client.compareChannelsClaims({
|
|
349
|
+
topic, channel_names: channels, days,
|
|
350
|
+
}));
|
|
351
|
+
await runHandler("compare_channels", { topic, channels, days });
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
"channel-contradictions": {
|
|
355
|
+
tool: "check_channel_contradictions",
|
|
356
|
+
description: "Contradictory claims a channel platforms on a topic (fast)",
|
|
357
|
+
specs: [FLAG_CHANNEL, FLAG_DAYS, FLAG_JSON],
|
|
358
|
+
run: async (args, json) => {
|
|
359
|
+
const [topic] = args.positionals;
|
|
360
|
+
if (!topic) {
|
|
361
|
+
print("Error: topic is required.");
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
const f = args.flags;
|
|
365
|
+
const channel = f.channel || undefined;
|
|
366
|
+
if (!channel) {
|
|
367
|
+
print("Error: --channel is required.");
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
const channels = channel.split(",").map((s) => s.trim()).filter(Boolean);
|
|
371
|
+
const days = Number(f.days) || undefined;
|
|
372
|
+
if (json)
|
|
373
|
+
return printJson(await client.checkChannelContradictionsClaims({
|
|
374
|
+
topic, channel_names: channels, days,
|
|
375
|
+
}));
|
|
376
|
+
await runHandler("check_channel_contradictions", { topic, channels, days });
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
"entities": {
|
|
380
|
+
tool: "find_entities",
|
|
381
|
+
description: "Fuzzy search canonical entities by name/alias (people, orgs, places, subjects)",
|
|
382
|
+
specs: [{ name: "type", type: "string" }, FLAG_LIMIT, FLAG_JSON],
|
|
383
|
+
run: async (args, json) => {
|
|
384
|
+
const [query] = args.positionals;
|
|
385
|
+
if (!query) {
|
|
386
|
+
print("Error: query is required.");
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
const f = args.flags;
|
|
390
|
+
const entityType = f.type || undefined;
|
|
391
|
+
const limit = Number(f.limit) || undefined;
|
|
392
|
+
if (json)
|
|
393
|
+
return printJson(await client.searchEntities({ query, entity_type: entityType, limit }));
|
|
394
|
+
await runHandler("find_entities", { query, entity_type: entityType, limit });
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
"entity-claims": {
|
|
398
|
+
tool: "entity_claims",
|
|
399
|
+
description: "Claims about a resolved canonical entity, ranked, with stance distribution",
|
|
400
|
+
specs: [{ name: "sort", type: "string", default: "spread" }, { name: "stance", type: "string" },
|
|
401
|
+
FLAG_LIMIT, FLAG_OFFSET, FLAG_JSON],
|
|
402
|
+
run: async (args, json) => {
|
|
403
|
+
const [name] = args.positionals;
|
|
404
|
+
if (!name) {
|
|
405
|
+
print("Error: entity name is required.");
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
const f = args.flags;
|
|
409
|
+
const params = {
|
|
410
|
+
entity: name, sort: f.sort || undefined, stance: f.stance || undefined,
|
|
411
|
+
limit: Number(f.limit) || undefined, offset: Number(f.offset) || undefined,
|
|
412
|
+
};
|
|
413
|
+
if (json)
|
|
414
|
+
return printJson(await client.getEntityClaims({
|
|
415
|
+
name, sort: params.sort, stance: params.stance, limit: params.limit, offset: params.offset,
|
|
416
|
+
}));
|
|
417
|
+
await runHandler("entity_claims", params);
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
"claim-graph": {
|
|
421
|
+
tool: "claim_graph",
|
|
422
|
+
description: "A claim's parent, children, and sibling (variant) claims",
|
|
423
|
+
specs: [FLAG_JSON],
|
|
424
|
+
run: async (args, json) => {
|
|
425
|
+
const [id] = args.positionals;
|
|
426
|
+
if (!id || !/^\d+$/.test(id)) {
|
|
427
|
+
print("Error: claim_id (numeric) required.");
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
const claimId = Number(id);
|
|
431
|
+
if (json)
|
|
432
|
+
return printJson(await client.getClaimGraph(claimId));
|
|
433
|
+
await runHandler("claim_graph", { claim_id: claimId });
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
"top-claims": {
|
|
437
|
+
tool: "top_claims",
|
|
438
|
+
description: "Leaderboard of the most widely-spread or most-contested claims",
|
|
439
|
+
specs: [{ name: "metric", type: "string", default: "spread" }, { name: "category", type: "string" },
|
|
440
|
+
{ name: "min-obs", type: "number", default: 2 }, FLAG_DAYS, FLAG_LIMIT, FLAG_JSON],
|
|
441
|
+
run: async (args, json) => {
|
|
442
|
+
const f = args.flags;
|
|
443
|
+
const params = {
|
|
444
|
+
metric: f.metric || undefined, category: f.category || undefined,
|
|
445
|
+
days: Number(f.days) || undefined, min_observations: Number(f["min-obs"]) || undefined,
|
|
446
|
+
limit: Number(f.limit) || undefined,
|
|
447
|
+
};
|
|
448
|
+
if (json)
|
|
449
|
+
return printJson(await client.getTopClaims(params));
|
|
450
|
+
await runHandler("top_claims", params);
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
claim: {
|
|
454
|
+
tool: "get_claim",
|
|
455
|
+
description: "Get full claim detail with observations and related claims",
|
|
456
|
+
specs: [{ name: "observations", type: "boolean" }, { name: "related", type: "boolean" }, FLAG_JSON],
|
|
457
|
+
run: async (args, json) => {
|
|
458
|
+
const [id] = args.positionals;
|
|
459
|
+
if (!id || !/^\d+$/.test(id)) {
|
|
460
|
+
print("Error: claim_id (numeric) required.");
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
const claimId = Number(id);
|
|
464
|
+
if (json) {
|
|
465
|
+
const detail = await client.getClaim(claimId);
|
|
466
|
+
if (args.flags.observations)
|
|
467
|
+
detail.observations = await client.getClaimObservations({ claim_id: claimId });
|
|
468
|
+
if (args.flags.related)
|
|
469
|
+
detail.related = await client.getRelatedClaims(claimId);
|
|
470
|
+
return printJson(detail);
|
|
471
|
+
}
|
|
472
|
+
await runHandler("get_claim", {
|
|
473
|
+
claim_id: claimId,
|
|
474
|
+
include_observations: args.flags.observations,
|
|
475
|
+
include_related: args.flags.related,
|
|
476
|
+
});
|
|
477
|
+
},
|
|
478
|
+
},
|
|
155
479
|
trend: {
|
|
156
480
|
tool: "detect_trend",
|
|
157
481
|
description: "Detect topic volume trends",
|
|
158
|
-
specs: [
|
|
482
|
+
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_LANGUAGE, FLAG_COUNTRY,
|
|
483
|
+
{ name: "recent", type: "number", default: 7 },
|
|
159
484
|
{ name: "baseline", type: "number", default: 30 },
|
|
160
485
|
{ name: "min-similarity", type: "number" }, FLAG_JSON],
|
|
161
486
|
run: async (args, json) => {
|
|
@@ -165,19 +490,57 @@ const COMMANDS = {
|
|
|
165
490
|
process.exit(1);
|
|
166
491
|
}
|
|
167
492
|
const f = args.flags;
|
|
493
|
+
const trendChannel = f.channel || undefined;
|
|
168
494
|
const params = {
|
|
169
|
-
topic,
|
|
495
|
+
topic, speaker: f.speaker || undefined,
|
|
496
|
+
channel: trendChannel,
|
|
497
|
+
language: f.language || undefined,
|
|
498
|
+
country: f.country || undefined,
|
|
499
|
+
recent_days: Number(f.recent) || 7, baseline_days: Number(f.baseline) || 30,
|
|
170
500
|
min_similarity: Number(f["min-similarity"]) || undefined,
|
|
171
501
|
};
|
|
172
502
|
if (json)
|
|
173
|
-
return printJson(await client.detectTrend(
|
|
503
|
+
return printJson(await client.detectTrend({
|
|
504
|
+
...params,
|
|
505
|
+
channel_names: trendChannel?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
506
|
+
}));
|
|
174
507
|
await runHandler("detect_trend", params);
|
|
175
508
|
},
|
|
176
509
|
},
|
|
510
|
+
timeline: {
|
|
511
|
+
tool: "get_timeline",
|
|
512
|
+
description: "Topic mention volume over time",
|
|
513
|
+
specs: [FLAG_DAYS, FLAG_SPEAKER, FLAG_CHANNEL, FLAG_LANGUAGE,
|
|
514
|
+
{ name: "granularity", alias: "g", type: "string", default: "day" },
|
|
515
|
+
FLAG_START, FLAG_END, FLAG_JSON],
|
|
516
|
+
run: async (args, json) => {
|
|
517
|
+
const [topic] = args.positionals;
|
|
518
|
+
if (!topic) {
|
|
519
|
+
print("Error: topic is required.");
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
const f = args.flags;
|
|
523
|
+
const days = Number(f.days) || 30;
|
|
524
|
+
const dr = f.start ? { start_date: f.start, end_date: f.end || new Date().toISOString().slice(0, 10) } : daysRange(days);
|
|
525
|
+
const tlChannel = f.channel || undefined;
|
|
526
|
+
const tlParams = {
|
|
527
|
+
topic,
|
|
528
|
+
speaker: f.speaker || undefined,
|
|
529
|
+
channel_names: tlChannel?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
530
|
+
start_date: dr.start_date,
|
|
531
|
+
end_date: dr.end_date,
|
|
532
|
+
language: f.language || undefined,
|
|
533
|
+
granularity: f.granularity || "day",
|
|
534
|
+
};
|
|
535
|
+
if (json)
|
|
536
|
+
return printJson(await client.getTimeline(tlParams));
|
|
537
|
+
await runHandler("get_timeline", { ...tlParams, days, channel: tlChannel });
|
|
538
|
+
},
|
|
539
|
+
},
|
|
177
540
|
speaker: {
|
|
178
541
|
tool: "find_speakers",
|
|
179
542
|
description: "Speaker search, profile, or discourse",
|
|
180
|
-
specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_OFFSET,
|
|
543
|
+
specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_OFFSET, FLAG_CHANNEL,
|
|
181
544
|
{ name: "segments", type: "boolean" }, FLAG_JSON],
|
|
182
545
|
run: async (args, json) => {
|
|
183
546
|
const [identity, topic] = args.positionals;
|
|
@@ -189,19 +552,21 @@ const COMMANDS = {
|
|
|
189
552
|
const isId = /^\d+$/.test(identity);
|
|
190
553
|
const days = Number(f.days) || 30;
|
|
191
554
|
const limit = Number(f.limit) || 20;
|
|
555
|
+
const offset = Number(f.offset) || 0;
|
|
556
|
+
const channelName = f.channel || undefined;
|
|
192
557
|
// Profile by ID
|
|
193
558
|
if (isId) {
|
|
194
559
|
const sid = Number(identity);
|
|
195
560
|
if (f.segments) {
|
|
196
|
-
const params = { speaker_id: sid, sort: "chronological", days, limit, offset:
|
|
561
|
+
const params = { speaker_id: sid, sort: "chronological", days, limit, offset, channel: channelName };
|
|
197
562
|
if (json)
|
|
198
|
-
return printJson(await client.getSpeakerSegments({ identity_id: sid, days, limit, offset
|
|
563
|
+
return printJson(await client.getSpeakerSegments({ identity_id: sid, days, limit, offset }));
|
|
199
564
|
await runHandler("search_quotes", params);
|
|
200
565
|
}
|
|
201
566
|
else if (topic) {
|
|
202
|
-
const params = { speaker_id: sid, topic, days, limit };
|
|
567
|
+
const params = { speaker_id: sid, topic, days, limit, offset, channel: channelName };
|
|
203
568
|
if (json)
|
|
204
|
-
return printJson(await client.searchSpeakerSegments({ identity_id: sid, query: topic, days, limit }));
|
|
569
|
+
return printJson(await client.searchSpeakerSegments({ identity_id: sid, query: topic, days, limit, offset }));
|
|
205
570
|
await runHandler("search_quotes", params);
|
|
206
571
|
}
|
|
207
572
|
else {
|
|
@@ -213,12 +578,12 @@ const COMMANDS = {
|
|
|
213
578
|
}
|
|
214
579
|
// Segments by name
|
|
215
580
|
if (f.segments) {
|
|
216
|
-
const params = {
|
|
581
|
+
const params = { speaker: identity, sort: "chronological", days, limit, offset, channel: channelName };
|
|
217
582
|
if (json) {
|
|
218
583
|
const byName = await client.getSpeakerByName(identity);
|
|
219
584
|
const sid = byName.speaker_id ?? byName.id;
|
|
220
585
|
if (sid)
|
|
221
|
-
return printJson(await client.getSpeakerSegments({ identity_id: sid, days, limit, offset
|
|
586
|
+
return printJson(await client.getSpeakerSegments({ identity_id: sid, days, limit, offset }));
|
|
222
587
|
return printJson({ error: `Speaker "${identity}" not found` });
|
|
223
588
|
}
|
|
224
589
|
await runHandler("search_quotes", params);
|
|
@@ -226,9 +591,9 @@ const COMMANDS = {
|
|
|
226
591
|
}
|
|
227
592
|
// Discourse search (name + topic)
|
|
228
593
|
if (topic) {
|
|
229
|
-
const params = {
|
|
594
|
+
const params = { speaker: identity, topic, days, limit, offset, channel: channelName };
|
|
230
595
|
if (json)
|
|
231
|
-
return printJson(await client.searchQuotes({ topic, speaker_name: identity, limit }));
|
|
596
|
+
return printJson(await client.searchQuotes({ topic, speaker_name: identity, channel_names: channelName ? [channelName] : undefined, limit, offset }));
|
|
232
597
|
await runHandler("search_quotes", params);
|
|
233
598
|
return;
|
|
234
599
|
}
|
|
@@ -248,8 +613,8 @@ const COMMANDS = {
|
|
|
248
613
|
},
|
|
249
614
|
channel: {
|
|
250
615
|
tool: "get_channel",
|
|
251
|
-
description: "Channel profile",
|
|
252
|
-
specs: [FLAG_JSON],
|
|
616
|
+
description: "Channel profile or episode listing",
|
|
617
|
+
specs: [{ name: "episodes", type: "boolean" }, FLAG_DAYS, FLAG_LIMIT, FLAG_OFFSET, FLAG_JSON],
|
|
253
618
|
run: async (args, json) => {
|
|
254
619
|
const [id] = args.positionals;
|
|
255
620
|
if (!id) {
|
|
@@ -257,9 +622,28 @@ const COMMANDS = {
|
|
|
257
622
|
process.exit(1);
|
|
258
623
|
}
|
|
259
624
|
const isId = /^\d+$/.test(id);
|
|
625
|
+
const f = args.flags;
|
|
626
|
+
if (f.episodes) {
|
|
627
|
+
const channelName = isId ? undefined : id;
|
|
628
|
+
const days = Number(f.days) || 30;
|
|
629
|
+
const limit = Number(f.limit) || 20;
|
|
630
|
+
const offset = Number(f.offset) || 0;
|
|
631
|
+
const dr = daysRange(days);
|
|
632
|
+
const params = {
|
|
633
|
+
channel: channelName, topic: undefined, sort: "chronological",
|
|
634
|
+
days, ...dr, limit, offset,
|
|
635
|
+
};
|
|
636
|
+
if (json)
|
|
637
|
+
return printJson(await client.searchQuotes({
|
|
638
|
+
channel_names: channelName ? [channelName] : undefined, start_date: dr.start_date, end_date: dr.end_date,
|
|
639
|
+
limit, offset,
|
|
640
|
+
}));
|
|
641
|
+
await runHandler("search_quotes", params);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
260
644
|
if (json)
|
|
261
645
|
return printJson(isId ? await client.getChannel(Number(id)) : await client.getChannelByName(id));
|
|
262
|
-
await runHandler("get_channel", isId ? { channel_id: Number(id) } : {
|
|
646
|
+
await runHandler("get_channel", isId ? { channel_id: Number(id) } : { channel: id });
|
|
263
647
|
},
|
|
264
648
|
},
|
|
265
649
|
episode: {
|
|
@@ -291,9 +675,10 @@ const COMMANDS = {
|
|
|
291
675
|
print("Error: segment ID required.");
|
|
292
676
|
process.exit(1);
|
|
293
677
|
}
|
|
678
|
+
const segId = parseSegmentId(id);
|
|
294
679
|
if (json)
|
|
295
|
-
return printJson(await client.getSegment(
|
|
296
|
-
await runHandler("get_segment", { segment_id:
|
|
680
|
+
return printJson(await client.getSegment(segId));
|
|
681
|
+
await runHandler("get_segment", { segment_id: segId });
|
|
297
682
|
},
|
|
298
683
|
},
|
|
299
684
|
thread: {
|
|
@@ -306,16 +691,17 @@ const COMMANDS = {
|
|
|
306
691
|
print("Error: segment ID required.");
|
|
307
692
|
process.exit(1);
|
|
308
693
|
}
|
|
694
|
+
const segId = parseSegmentId(id);
|
|
309
695
|
const n = Number(args.flags.n) || 5;
|
|
310
696
|
if (json)
|
|
311
|
-
return printJson(await client.getSegmentThread(
|
|
312
|
-
await runHandler("get_thread", { segment_id:
|
|
697
|
+
return printJson(await client.getSegmentThread(segId, n));
|
|
698
|
+
await runHandler("get_thread", { segment_id: segId, n });
|
|
313
699
|
},
|
|
314
700
|
},
|
|
315
701
|
analyze: {
|
|
316
702
|
tool: "analyze_topic",
|
|
317
703
|
description: "AI-powered topic analysis",
|
|
318
|
-
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_DAYS,
|
|
704
|
+
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_DAYS, FLAG_LANGUAGE, FLAG_COUNTRY,
|
|
319
705
|
{ name: "workflow", alias: "w", type: "string", default: "simple_rag" },
|
|
320
706
|
FLAG_SKIP_CACHE, FLAG_JSON],
|
|
321
707
|
run: async (args, json) => {
|
|
@@ -326,10 +712,13 @@ const COMMANDS = {
|
|
|
326
712
|
}
|
|
327
713
|
const f = args.flags;
|
|
328
714
|
process.stderr.write("Analyzing... (this may take up to 60 seconds)\n");
|
|
715
|
+
const channelArg = f.channel || undefined;
|
|
329
716
|
const params = {
|
|
330
717
|
query, workflow: f.workflow,
|
|
331
|
-
|
|
332
|
-
|
|
718
|
+
speaker: f.speaker || undefined,
|
|
719
|
+
channels: channelArg?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
720
|
+
language: f.language || undefined,
|
|
721
|
+
country: f.country || undefined,
|
|
333
722
|
days: Number(f.days) || undefined,
|
|
334
723
|
skip_cache: f["skip-cache"],
|
|
335
724
|
};
|
|
@@ -338,10 +727,39 @@ const COMMANDS = {
|
|
|
338
727
|
await runHandler("analyze_topic", params);
|
|
339
728
|
},
|
|
340
729
|
},
|
|
730
|
+
frames: {
|
|
731
|
+
tool: "compare_frames",
|
|
732
|
+
description: "Compare left vs right media framing on a topic",
|
|
733
|
+
specs: [FLAG_DAYS, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_SKIP_CACHE, FLAG_JSON],
|
|
734
|
+
run: async (args, json) => {
|
|
735
|
+
const [topic] = args.positionals;
|
|
736
|
+
if (!topic) {
|
|
737
|
+
print("Error: topic is required.");
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
const f = args.flags;
|
|
741
|
+
process.stderr.write("Comparing left vs right framing... (this may take up to 60 seconds)\n");
|
|
742
|
+
const params = {
|
|
743
|
+
topic, days: Number(f.days) || 30,
|
|
744
|
+
language: f.language || undefined,
|
|
745
|
+
country: f.country || undefined,
|
|
746
|
+
skip_cache: f["skip-cache"],
|
|
747
|
+
};
|
|
748
|
+
if (json) {
|
|
749
|
+
const shared = { query: topic, workflow: "deep_analysis", days: params.days, language: params.language, country: params.country, skip_cache: params.skip_cache };
|
|
750
|
+
const [left, right] = await Promise.all([
|
|
751
|
+
client.analyze({ ...shared, political_lean_min: 0.0, political_lean_max: 0.35 }),
|
|
752
|
+
client.analyze({ ...shared, political_lean_min: 0.65, political_lean_max: 1.0 }),
|
|
753
|
+
]);
|
|
754
|
+
return printJson({ left, right });
|
|
755
|
+
}
|
|
756
|
+
await runHandler("compare_frames", params);
|
|
757
|
+
},
|
|
758
|
+
},
|
|
341
759
|
compare: {
|
|
342
760
|
tool: "compare_speakers",
|
|
343
761
|
description: "Compare speakers on a topic",
|
|
344
|
-
specs: [{ name: "speakers", type: "string" }, FLAG_DAYS, FLAG_SKIP_CACHE, FLAG_JSON],
|
|
762
|
+
specs: [{ name: "speakers", type: "string" }, FLAG_SPEAKER, FLAG_DAYS, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_SKIP_CACHE, FLAG_JSON],
|
|
345
763
|
run: async (args, json) => {
|
|
346
764
|
const [topic] = args.positionals;
|
|
347
765
|
if (!topic) {
|
|
@@ -349,17 +767,25 @@ const COMMANDS = {
|
|
|
349
767
|
process.exit(1);
|
|
350
768
|
}
|
|
351
769
|
const f = args.flags;
|
|
352
|
-
const
|
|
770
|
+
const speakersStr = f.speakers || f.speaker || "";
|
|
771
|
+
const speakers = speakersStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
353
772
|
if (speakers.length < 2) {
|
|
354
773
|
print("Error: --speakers requires 2-5 comma-separated names.");
|
|
355
774
|
process.exit(1);
|
|
356
775
|
}
|
|
357
776
|
process.stderr.write("Comparing speakers... (this may take up to 60 seconds)\n");
|
|
358
777
|
const params = {
|
|
359
|
-
topic, speakers, days: Number(f.days) || 90,
|
|
778
|
+
topic, speakers, days: Number(f.days) || 90,
|
|
779
|
+
language: f.language || undefined,
|
|
780
|
+
country: f.country || undefined,
|
|
781
|
+
skip_cache: f["skip-cache"],
|
|
360
782
|
};
|
|
361
783
|
if (json)
|
|
362
|
-
return printJson(await client.analyze({
|
|
784
|
+
return printJson(await client.analyze({
|
|
785
|
+
query: topic, workflow: "speaker_compare", speaker_names: speakers,
|
|
786
|
+
language: params.language, country: params.country,
|
|
787
|
+
days: params.days, skip_cache: params.skip_cache,
|
|
788
|
+
}));
|
|
363
789
|
await runHandler("compare_speakers", params);
|
|
364
790
|
},
|
|
365
791
|
},
|
|
@@ -380,7 +806,7 @@ const COMMANDS = {
|
|
|
380
806
|
}
|
|
381
807
|
process.stderr.write("Building dossier... (this may take up to 60 seconds)\n");
|
|
382
808
|
const params = {
|
|
383
|
-
topic,
|
|
809
|
+
topic, speaker: speaker, days: Number(args.flags.days) || 180, skip_cache: args.flags["skip-cache"],
|
|
384
810
|
};
|
|
385
811
|
if (json)
|
|
386
812
|
return printJson(await client.analyze({ query: topic, workflow: "speaker_dossier", speaker_name: speaker, days: params.days, skip_cache: params.skip_cache }));
|
|
@@ -404,7 +830,7 @@ const COMMANDS = {
|
|
|
404
830
|
}
|
|
405
831
|
process.stderr.write("Checking contradictions...\n");
|
|
406
832
|
const params = {
|
|
407
|
-
topic,
|
|
833
|
+
topic, speaker: speaker, days: Number(args.flags.days) || 365, skip_cache: args.flags["skip-cache"],
|
|
408
834
|
};
|
|
409
835
|
if (json)
|
|
410
836
|
return printJson(await client.analyze({ query: topic, workflow: "contradiction_check", speaker_name: speaker, days: params.days, skip_cache: params.skip_cache }));
|
|
@@ -448,7 +874,7 @@ const COMMANDS = {
|
|
|
448
874
|
specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_JSON],
|
|
449
875
|
run: async (args, json) => {
|
|
450
876
|
const f = args.flags;
|
|
451
|
-
const params = { days: Number(f.days) || 30, limit: Number(f.limit) ||
|
|
877
|
+
const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 20, language: f.language || undefined, country: f.country || undefined };
|
|
452
878
|
if (json)
|
|
453
879
|
return printJson(await client.getTopChannels(params));
|
|
454
880
|
await runHandler("get_top_channels", params);
|
|
@@ -457,10 +883,11 @@ const COMMANDS = {
|
|
|
457
883
|
"top-speakers": {
|
|
458
884
|
tool: "get_top_speakers",
|
|
459
885
|
description: "Top speakers by appearances, talk time, or clips",
|
|
460
|
-
specs: [FLAG_DAYS, FLAG_LIMIT,
|
|
886
|
+
specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_LANGUAGE, FLAG_COUNTRY,
|
|
887
|
+
{ name: "ranking", alias: "r", type: "string", default: "appearances" }, FLAG_JSON],
|
|
461
888
|
run: async (args, json) => {
|
|
462
889
|
const f = args.flags;
|
|
463
|
-
const params = { days: Number(f.days) || 30, limit: Number(f.limit) ||
|
|
890
|
+
const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 20, ranking: f.ranking, language: f.language || undefined, country: f.country || undefined };
|
|
464
891
|
if (json)
|
|
465
892
|
return printJson(await client.getTopSpeakers(params));
|
|
466
893
|
await runHandler("get_top_speakers", params);
|
|
@@ -481,15 +908,18 @@ const COMMANDS = {
|
|
|
481
908
|
topics: {
|
|
482
909
|
tool: "get_topics",
|
|
483
910
|
description: "Trending topic clusters",
|
|
484
|
-
specs: [FLAG_LANGUAGE, FLAG_LIMIT, { name: "query", alias: "q", type: "string" },
|
|
911
|
+
specs: [FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_LIMIT, FLAG_DAYS, { name: "query", alias: "q", type: "string" },
|
|
485
912
|
{ name: "min-segments", type: "number", default: 50 },
|
|
486
913
|
{ name: "start", type: "string" }, { name: "end", type: "string" }, FLAG_JSON],
|
|
487
914
|
run: async (args, json) => {
|
|
488
915
|
const f = args.flags;
|
|
916
|
+
const dr = f.days ? daysRange(Number(f.days)) : undefined;
|
|
489
917
|
const params = {
|
|
490
918
|
language: f.language || "en", limit: Number(f.limit) || 20,
|
|
491
|
-
query: f.query || undefined,
|
|
492
|
-
|
|
919
|
+
query: f.query || undefined, country: f.country || undefined,
|
|
920
|
+
min_segments: Number(f["min-segments"]) || 50,
|
|
921
|
+
start_date: f.start || dr?.start_date || undefined,
|
|
922
|
+
end_date: f.end || dr?.end_date || undefined,
|
|
493
923
|
};
|
|
494
924
|
if (json)
|
|
495
925
|
return printJson(await client.getTopics(params));
|
|
@@ -501,13 +931,14 @@ const COMMANDS = {
|
|
|
501
931
|
description: "Ad intelligence",
|
|
502
932
|
specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_OFFSET,
|
|
503
933
|
{ name: "advertisers", type: "boolean" }, { name: "trending", type: "boolean" },
|
|
504
|
-
|
|
934
|
+
FLAG_CHANNEL,
|
|
505
935
|
{ name: "category", type: "string" }, { name: "sort", type: "string", default: "episodes" },
|
|
506
936
|
FLAG_JSON],
|
|
507
937
|
run: async (args, json) => {
|
|
508
938
|
const [adId] = args.positionals;
|
|
509
939
|
const f = args.flags;
|
|
510
940
|
const days = Number(f.days) || 30;
|
|
941
|
+
const channelArg = f.channel || undefined;
|
|
511
942
|
if (adId && /^\d+$/.test(adId)) {
|
|
512
943
|
if (json)
|
|
513
944
|
return printJson(await client.getAdvertiser(Number(adId)));
|
|
@@ -523,10 +954,24 @@ const COMMANDS = {
|
|
|
523
954
|
return printJson(await client.getAdTrends(days));
|
|
524
955
|
await runHandler("get_ad_analytics", { view: "trending", days });
|
|
525
956
|
}
|
|
526
|
-
else if (
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
957
|
+
else if (channelArg) {
|
|
958
|
+
const channelId = /^\d+$/.test(channelArg) ? Number(channelArg) : undefined;
|
|
959
|
+
if (channelId) {
|
|
960
|
+
if (json)
|
|
961
|
+
return printJson(await client.getAdsByChannel(channelId, days));
|
|
962
|
+
await runHandler("get_ad_analytics", { view: "channel", channel_id: channelId, days });
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
const profile = await client.getChannelByName(channelArg);
|
|
966
|
+
const resolvedId = profile.channel_id ?? profile.id;
|
|
967
|
+
if (!resolvedId) {
|
|
968
|
+
print(`Error: channel "${channelArg}" not found.`);
|
|
969
|
+
process.exit(1);
|
|
970
|
+
}
|
|
971
|
+
if (json)
|
|
972
|
+
return printJson(await client.getAdsByChannel(resolvedId, days));
|
|
973
|
+
await runHandler("get_ad_analytics", { view: "channel", channel_id: resolvedId, days });
|
|
974
|
+
}
|
|
530
975
|
}
|
|
531
976
|
else {
|
|
532
977
|
if (json)
|
|
@@ -572,6 +1017,8 @@ const COMMANDS = {
|
|
|
572
1017
|
},
|
|
573
1018
|
},
|
|
574
1019
|
};
|
|
1020
|
+
// The backend's whoami guide advertises "quotes" — keep it working as an alias.
|
|
1021
|
+
COMMANDS.quotes = COMMANDS.search;
|
|
575
1022
|
// ---------------------------------------------------------------------------
|
|
576
1023
|
// Setup command
|
|
577
1024
|
// ---------------------------------------------------------------------------
|