@signal4/s4 2.2.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +414 -37
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +151 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +214 -8
- 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 +454 -8
- 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 +28 -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 +189 -12
- 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 +29 -12
- 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 +336 -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) {
|
|
@@ -63,6 +69,15 @@ function daysRange(days) {
|
|
|
63
69
|
const start = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
|
|
64
70
|
return { start_date: start, end_date: end };
|
|
65
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
|
+
}
|
|
66
81
|
function print(text) {
|
|
67
82
|
process.stdout.write(text + "\n");
|
|
68
83
|
}
|
|
@@ -104,9 +119,9 @@ const COMMANDS = {
|
|
|
104
119
|
const dr = f.start ? { start_date: f.start, end_date: f.end || new Date().toISOString().slice(0, 10) } : daysRange(days);
|
|
105
120
|
const fullHistory = f["full-history"];
|
|
106
121
|
const params = {
|
|
107
|
-
topic,
|
|
122
|
+
topic, speaker: f.speaker || undefined,
|
|
108
123
|
speaker_id: Number(f["speaker-id"]) || undefined,
|
|
109
|
-
|
|
124
|
+
channel: f.channel || undefined,
|
|
110
125
|
language: f.language || undefined,
|
|
111
126
|
country: f.country || undefined,
|
|
112
127
|
days, ...dr,
|
|
@@ -116,13 +131,15 @@ const COMMANDS = {
|
|
|
116
131
|
};
|
|
117
132
|
if (json)
|
|
118
133
|
return printJson(await client.searchQuotes({
|
|
119
|
-
topic: params.topic, speaker_name: params.
|
|
120
|
-
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,
|
|
121
137
|
start_date: params.start_date, end_date: params.end_date,
|
|
122
138
|
language: params.language, country: params.country,
|
|
123
139
|
mode: params.mode !== "semantic" ? params.mode : undefined,
|
|
124
|
-
limit: params.
|
|
140
|
+
limit: params.limit, offset: params.offset,
|
|
125
141
|
full_history: fullHistory || undefined,
|
|
142
|
+
count_only: params.count_only || undefined,
|
|
126
143
|
}));
|
|
127
144
|
await runHandler("search_quotes", params);
|
|
128
145
|
},
|
|
@@ -135,6 +152,9 @@ const COMMANDS = {
|
|
|
135
152
|
FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_MODE, FLAG_KEYWORD,
|
|
136
153
|
{ name: "sort", type: "string", default: "relevance" },
|
|
137
154
|
{ name: "min-obs", type: "number", default: 1 },
|
|
155
|
+
{ name: "min-similarity", type: "number" },
|
|
156
|
+
{ name: "political-lean-min", type: "number" }, { name: "political-lean-max", type: "number" },
|
|
157
|
+
{ name: "source", type: "string" },
|
|
138
158
|
FLAG_DAYS, FLAG_LIMIT, FLAG_OFFSET, FLAG_START, FLAG_END, FLAG_JSON],
|
|
139
159
|
run: async (args, json) => {
|
|
140
160
|
const [positionalTopic] = args.positionals;
|
|
@@ -144,33 +164,319 @@ const COMMANDS = {
|
|
|
144
164
|
const mode = keyword ? "keyword" : f.mode;
|
|
145
165
|
const days = Number(f.days) || 30;
|
|
146
166
|
const dr = f.start ? { start_date: f.start, end_date: f.end || new Date().toISOString().slice(0, 10) } : undefined;
|
|
167
|
+
const leanMin = f["political-lean-min"] !== undefined ? Number(f["political-lean-min"]) : undefined;
|
|
168
|
+
const leanMax = f["political-lean-max"] !== undefined ? Number(f["political-lean-max"]) : undefined;
|
|
169
|
+
const minSim = f["min-similarity"] !== undefined ? Number(f["min-similarity"]) : undefined;
|
|
147
170
|
const params = {
|
|
148
171
|
topic, entity: f.entity || undefined,
|
|
149
|
-
|
|
150
|
-
|
|
172
|
+
speaker: f.speaker || undefined,
|
|
173
|
+
channel: f.channel || undefined,
|
|
151
174
|
category: f.category || undefined,
|
|
152
175
|
stance: f.stance || undefined,
|
|
153
176
|
language: f.language || undefined,
|
|
154
177
|
country: f.country || undefined,
|
|
178
|
+
political_lean_min: leanMin, political_lean_max: leanMax,
|
|
179
|
+
min_similarity: minSim,
|
|
180
|
+
source: f.source || undefined,
|
|
155
181
|
days, mode,
|
|
156
182
|
sort: f.sort, min_observations: Number(f["min-obs"]) || 1,
|
|
157
183
|
limit: Number(f.limit), offset: Number(f.offset),
|
|
158
184
|
...dr,
|
|
159
185
|
};
|
|
186
|
+
const claimsDr = dr ?? daysRange(days);
|
|
160
187
|
if (json)
|
|
161
188
|
return printJson(await client.searchClaims({
|
|
162
|
-
topic: params.topic, entity: params.entity, speaker_name: params.
|
|
163
|
-
channel_names: params.
|
|
164
|
-
|
|
189
|
+
topic: params.topic, entity: params.entity, speaker_name: params.speaker,
|
|
190
|
+
channel_names: params.channel ? params.channel.split(",").map((s) => s.trim()).filter(Boolean) : undefined,
|
|
191
|
+
category: params.category, stance: params.stance, language: params.language,
|
|
165
192
|
country: params.country,
|
|
166
|
-
|
|
167
|
-
|
|
193
|
+
political_lean_min: leanMin, political_lean_max: leanMax,
|
|
194
|
+
min_similarity: minSim,
|
|
195
|
+
start_date: claimsDr.start_date, end_date: claimsDr.end_date,
|
|
196
|
+
mode: params.mode, sort: params.sort,
|
|
168
197
|
min_observations: params.min_observations > 1 ? params.min_observations : undefined,
|
|
169
198
|
limit: params.limit, offset: params.offset,
|
|
170
199
|
}));
|
|
171
200
|
await runHandler("search_claims", params);
|
|
172
201
|
},
|
|
173
202
|
},
|
|
203
|
+
"analyze-claims": {
|
|
204
|
+
tool: "analyze_claims",
|
|
205
|
+
description: "Analyze a topic over extracted claims — themes with observation stats (fast, no LLM)",
|
|
206
|
+
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_LANGUAGE, FLAG_DAYS,
|
|
207
|
+
{ name: "min-obs", type: "number", default: 1 }, FLAG_JSON],
|
|
208
|
+
run: async (args, json) => {
|
|
209
|
+
const [topic] = args.positionals;
|
|
210
|
+
if (!topic) {
|
|
211
|
+
print("Error: topic is required.");
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
const f = args.flags;
|
|
215
|
+
const channelArg = f.channel || undefined;
|
|
216
|
+
const params = {
|
|
217
|
+
topic,
|
|
218
|
+
speaker: f.speaker || undefined,
|
|
219
|
+
channels: channelArg?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
220
|
+
language: f.language || undefined,
|
|
221
|
+
days: Number(f.days) || undefined,
|
|
222
|
+
min_observations: Number(f["min-obs"]) || undefined,
|
|
223
|
+
};
|
|
224
|
+
if (json)
|
|
225
|
+
return printJson(await client.analyzeTopicClaims({
|
|
226
|
+
topic, speaker_name: params.speaker, channel_names: params.channels,
|
|
227
|
+
language: params.language, days: params.days, min_observations: params.min_observations,
|
|
228
|
+
}));
|
|
229
|
+
await runHandler("analyze_claims", params);
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
"trend-claims": {
|
|
233
|
+
tool: "trend_claims",
|
|
234
|
+
description: "Detect whether a topic is surging/fading by claim volume (fast)",
|
|
235
|
+
specs: [{ name: "recent-days", type: "number", default: 7 },
|
|
236
|
+
{ name: "baseline-days", type: "number", default: 30 },
|
|
237
|
+
{ name: "min-similarity", type: "number" }, FLAG_JSON],
|
|
238
|
+
run: async (args, json) => {
|
|
239
|
+
const [topic] = args.positionals;
|
|
240
|
+
if (!topic) {
|
|
241
|
+
print("Error: topic is required.");
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
const f = args.flags;
|
|
245
|
+
const params = {
|
|
246
|
+
topic,
|
|
247
|
+
recent_days: Number(f["recent-days"]) || undefined,
|
|
248
|
+
baseline_days: Number(f["baseline-days"]) || undefined,
|
|
249
|
+
min_similarity: f["min-similarity"] !== undefined ? Number(f["min-similarity"]) : undefined,
|
|
250
|
+
};
|
|
251
|
+
if (json)
|
|
252
|
+
return printJson(await client.detectTrendClaims(params));
|
|
253
|
+
await runHandler("trend_claims", params);
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
"contradict-claims": {
|
|
257
|
+
tool: "contradict_claims",
|
|
258
|
+
description: "Find a speaker's contradictory claims on a topic via stance analysis (fast)",
|
|
259
|
+
specs: [FLAG_SPEAKER, FLAG_DAYS, FLAG_JSON],
|
|
260
|
+
run: async (args, json) => {
|
|
261
|
+
const [topic] = args.positionals;
|
|
262
|
+
if (!topic) {
|
|
263
|
+
print("Error: topic is required.");
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
const f = args.flags;
|
|
267
|
+
const speaker = f.speaker || undefined;
|
|
268
|
+
if (!speaker) {
|
|
269
|
+
print("Error: --speaker is required.");
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
const params = { topic, speaker, days: Number(f.days) || undefined };
|
|
273
|
+
if (json)
|
|
274
|
+
return printJson(await client.checkContradictionsClaims({
|
|
275
|
+
topic, speaker_name: speaker, days: params.days,
|
|
276
|
+
}));
|
|
277
|
+
await runHandler("contradict_claims", params);
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
"compare-claims": {
|
|
281
|
+
tool: "compare_claims",
|
|
282
|
+
description: "Compare 2-5 speakers' claims on a topic — side-by-side stance (fast)",
|
|
283
|
+
specs: [{ name: "speakers", type: "string" }, FLAG_SPEAKER, FLAG_DAYS, FLAG_JSON],
|
|
284
|
+
run: async (args, json) => {
|
|
285
|
+
const [topic] = args.positionals;
|
|
286
|
+
if (!topic) {
|
|
287
|
+
print("Error: topic is required.");
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
const f = args.flags;
|
|
291
|
+
const speakersStr = f.speakers || f.speaker || "";
|
|
292
|
+
const speakers = speakersStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
293
|
+
if (speakers.length < 2) {
|
|
294
|
+
print("Error: --speakers requires 2-5 comma-separated names.");
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
const params = { topic, speakers, days: Number(f.days) || undefined };
|
|
298
|
+
if (json)
|
|
299
|
+
return printJson(await client.compareSpeakersClaims({
|
|
300
|
+
topic, speaker_names: speakers, days: params.days,
|
|
301
|
+
}));
|
|
302
|
+
await runHandler("compare_claims", params);
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
"channel-dossier": {
|
|
306
|
+
tool: "channel_dossier",
|
|
307
|
+
description: "Claims dossier of what a channel (show) platformed on a topic (fast)",
|
|
308
|
+
specs: [FLAG_CHANNEL, FLAG_DAYS, FLAG_JSON],
|
|
309
|
+
run: async (args, json) => {
|
|
310
|
+
const [topic] = args.positionals;
|
|
311
|
+
if (!topic) {
|
|
312
|
+
print("Error: topic is required.");
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
const f = args.flags;
|
|
316
|
+
const channel = f.channel || undefined;
|
|
317
|
+
if (!channel) {
|
|
318
|
+
print("Error: --channel is required.");
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
const channels = channel.split(",").map((s) => s.trim()).filter(Boolean);
|
|
322
|
+
const days = Number(f.days) || undefined;
|
|
323
|
+
if (json)
|
|
324
|
+
return printJson(await client.buildChannelDossier({
|
|
325
|
+
topic, channel_names: channels, days,
|
|
326
|
+
}));
|
|
327
|
+
await runHandler("channel_dossier", { topic, channels, days });
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
"compare-channels": {
|
|
331
|
+
tool: "compare_channels",
|
|
332
|
+
description: "Compare 2-5 channels' claims on a topic — stance + speaker overlap (fast)",
|
|
333
|
+
specs: [{ name: "channels", type: "string" }, FLAG_CHANNEL, FLAG_DAYS, FLAG_JSON],
|
|
334
|
+
run: async (args, json) => {
|
|
335
|
+
const [topic] = args.positionals;
|
|
336
|
+
if (!topic) {
|
|
337
|
+
print("Error: topic is required.");
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
const f = args.flags;
|
|
341
|
+
const channelsStr = f.channels || f.channel || "";
|
|
342
|
+
const channels = channelsStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
343
|
+
if (channels.length < 2) {
|
|
344
|
+
print("Error: --channels requires 2-5 comma-separated names.");
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
const days = Number(f.days) || undefined;
|
|
348
|
+
if (json)
|
|
349
|
+
return printJson(await client.compareChannelsClaims({
|
|
350
|
+
topic, channel_names: channels, days,
|
|
351
|
+
}));
|
|
352
|
+
await runHandler("compare_channels", { topic, channels, days });
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
"channel-contradictions": {
|
|
356
|
+
tool: "check_channel_contradictions",
|
|
357
|
+
description: "Contradictory claims a channel platforms on a topic (fast)",
|
|
358
|
+
specs: [FLAG_CHANNEL, FLAG_DAYS, FLAG_JSON],
|
|
359
|
+
run: async (args, json) => {
|
|
360
|
+
const [topic] = args.positionals;
|
|
361
|
+
if (!topic) {
|
|
362
|
+
print("Error: topic is required.");
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
const f = args.flags;
|
|
366
|
+
const channel = f.channel || undefined;
|
|
367
|
+
if (!channel) {
|
|
368
|
+
print("Error: --channel is required.");
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
const channels = channel.split(",").map((s) => s.trim()).filter(Boolean);
|
|
372
|
+
const days = Number(f.days) || undefined;
|
|
373
|
+
if (json)
|
|
374
|
+
return printJson(await client.checkChannelContradictionsClaims({
|
|
375
|
+
topic, channel_names: channels, days,
|
|
376
|
+
}));
|
|
377
|
+
await runHandler("check_channel_contradictions", { topic, channels, days });
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
"entities": {
|
|
381
|
+
tool: "find_entities",
|
|
382
|
+
description: "Fuzzy search canonical entities by name/alias (people, orgs, places, subjects)",
|
|
383
|
+
specs: [{ name: "type", type: "string" }, FLAG_LIMIT, FLAG_JSON],
|
|
384
|
+
run: async (args, json) => {
|
|
385
|
+
const [query] = args.positionals;
|
|
386
|
+
if (!query) {
|
|
387
|
+
print("Error: query is required.");
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
const f = args.flags;
|
|
391
|
+
const entityType = f.type || undefined;
|
|
392
|
+
const limit = Number(f.limit) || undefined;
|
|
393
|
+
if (json)
|
|
394
|
+
return printJson(await client.searchEntities({ query, entity_type: entityType, limit }));
|
|
395
|
+
await runHandler("find_entities", { query, entity_type: entityType, limit });
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
"entity-claims": {
|
|
399
|
+
tool: "entity_claims",
|
|
400
|
+
description: "Claims about a resolved canonical entity, ranked, with stance distribution",
|
|
401
|
+
specs: [{ name: "sort", type: "string", default: "spread" }, { name: "stance", type: "string" },
|
|
402
|
+
FLAG_LIMIT, FLAG_OFFSET, FLAG_JSON],
|
|
403
|
+
run: async (args, json) => {
|
|
404
|
+
const [name] = args.positionals;
|
|
405
|
+
if (!name) {
|
|
406
|
+
print("Error: entity name is required.");
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
const f = args.flags;
|
|
410
|
+
const params = {
|
|
411
|
+
entity: name, sort: f.sort || undefined, stance: f.stance || undefined,
|
|
412
|
+
limit: Number(f.limit) || undefined, offset: Number(f.offset) || undefined,
|
|
413
|
+
};
|
|
414
|
+
if (json)
|
|
415
|
+
return printJson(await client.getEntityClaims({
|
|
416
|
+
name, sort: params.sort, stance: params.stance, limit: params.limit, offset: params.offset,
|
|
417
|
+
}));
|
|
418
|
+
await runHandler("entity_claims", params);
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
"claim-graph": {
|
|
422
|
+
tool: "claim_graph",
|
|
423
|
+
description: "A claim's parent, children, and sibling (variant) claims",
|
|
424
|
+
specs: [FLAG_JSON],
|
|
425
|
+
run: async (args, json) => {
|
|
426
|
+
const [id] = args.positionals;
|
|
427
|
+
if (!id || !/^\d+$/.test(id)) {
|
|
428
|
+
print("Error: claim_id (numeric) required.");
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
const claimId = Number(id);
|
|
432
|
+
if (json)
|
|
433
|
+
return printJson(await client.getClaimGraph(claimId));
|
|
434
|
+
await runHandler("claim_graph", { claim_id: claimId });
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
"top-claims": {
|
|
438
|
+
tool: "top_claims",
|
|
439
|
+
description: "Leaderboard of the most widely-spread or most-contested claims",
|
|
440
|
+
specs: [{ name: "metric", type: "string", default: "spread" }, { name: "category", type: "string" },
|
|
441
|
+
{ name: "min-obs", type: "number", default: 2 }, FLAG_DAYS, FLAG_LIMIT, FLAG_JSON],
|
|
442
|
+
run: async (args, json) => {
|
|
443
|
+
const f = args.flags;
|
|
444
|
+
const params = {
|
|
445
|
+
metric: f.metric || undefined, category: f.category || undefined,
|
|
446
|
+
days: Number(f.days) || undefined, min_observations: Number(f["min-obs"]) || undefined,
|
|
447
|
+
limit: Number(f.limit) || undefined,
|
|
448
|
+
};
|
|
449
|
+
if (json)
|
|
450
|
+
return printJson(await client.getTopClaims(params));
|
|
451
|
+
await runHandler("top_claims", params);
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
claim: {
|
|
455
|
+
tool: "get_claim",
|
|
456
|
+
description: "Get full claim detail with observations and related claims",
|
|
457
|
+
specs: [{ name: "observations", type: "boolean" }, { name: "related", type: "boolean" }, FLAG_JSON],
|
|
458
|
+
run: async (args, json) => {
|
|
459
|
+
const [id] = args.positionals;
|
|
460
|
+
if (!id || !/^\d+$/.test(id)) {
|
|
461
|
+
print("Error: claim_id (numeric) required.");
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
const claimId = Number(id);
|
|
465
|
+
if (json) {
|
|
466
|
+
const detail = await client.getClaim(claimId);
|
|
467
|
+
if (args.flags.observations)
|
|
468
|
+
detail.observations = await client.getClaimObservations({ claim_id: claimId });
|
|
469
|
+
if (args.flags.related)
|
|
470
|
+
detail.related = await client.getRelatedClaims(claimId);
|
|
471
|
+
return printJson(detail);
|
|
472
|
+
}
|
|
473
|
+
await runHandler("get_claim", {
|
|
474
|
+
claim_id: claimId,
|
|
475
|
+
include_observations: args.flags.observations,
|
|
476
|
+
include_related: args.flags.related,
|
|
477
|
+
});
|
|
478
|
+
},
|
|
479
|
+
},
|
|
174
480
|
trend: {
|
|
175
481
|
tool: "detect_trend",
|
|
176
482
|
description: "Detect topic volume trends",
|
|
@@ -185,19 +491,53 @@ const COMMANDS = {
|
|
|
185
491
|
process.exit(1);
|
|
186
492
|
}
|
|
187
493
|
const f = args.flags;
|
|
494
|
+
const trendChannel = f.channel || undefined;
|
|
188
495
|
const params = {
|
|
189
|
-
topic,
|
|
190
|
-
|
|
496
|
+
topic, speaker: f.speaker || undefined,
|
|
497
|
+
channel: trendChannel,
|
|
191
498
|
language: f.language || undefined,
|
|
192
499
|
country: f.country || undefined,
|
|
193
500
|
recent_days: Number(f.recent) || 7, baseline_days: Number(f.baseline) || 30,
|
|
194
501
|
min_similarity: Number(f["min-similarity"]) || undefined,
|
|
195
502
|
};
|
|
196
503
|
if (json)
|
|
197
|
-
return printJson(await client.detectTrend(
|
|
504
|
+
return printJson(await client.detectTrend({
|
|
505
|
+
...params,
|
|
506
|
+
channel_names: trendChannel?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
507
|
+
}));
|
|
198
508
|
await runHandler("detect_trend", params);
|
|
199
509
|
},
|
|
200
510
|
},
|
|
511
|
+
timeline: {
|
|
512
|
+
tool: "get_timeline",
|
|
513
|
+
description: "Topic mention volume over time",
|
|
514
|
+
specs: [FLAG_DAYS, FLAG_SPEAKER, FLAG_CHANNEL, FLAG_LANGUAGE,
|
|
515
|
+
{ name: "granularity", alias: "g", type: "string", default: "day" },
|
|
516
|
+
FLAG_START, FLAG_END, FLAG_JSON],
|
|
517
|
+
run: async (args, json) => {
|
|
518
|
+
const [topic] = args.positionals;
|
|
519
|
+
if (!topic) {
|
|
520
|
+
print("Error: topic is required.");
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
const f = args.flags;
|
|
524
|
+
const days = Number(f.days) || 30;
|
|
525
|
+
const dr = f.start ? { start_date: f.start, end_date: f.end || new Date().toISOString().slice(0, 10) } : daysRange(days);
|
|
526
|
+
const tlChannel = f.channel || undefined;
|
|
527
|
+
const tlParams = {
|
|
528
|
+
topic,
|
|
529
|
+
speaker: f.speaker || undefined,
|
|
530
|
+
channel_names: tlChannel?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
531
|
+
start_date: dr.start_date,
|
|
532
|
+
end_date: dr.end_date,
|
|
533
|
+
language: f.language || undefined,
|
|
534
|
+
granularity: f.granularity || "day",
|
|
535
|
+
};
|
|
536
|
+
if (json)
|
|
537
|
+
return printJson(await client.getTimeline(tlParams));
|
|
538
|
+
await runHandler("get_timeline", { ...tlParams, days, channel: tlChannel });
|
|
539
|
+
},
|
|
540
|
+
},
|
|
201
541
|
speaker: {
|
|
202
542
|
tool: "find_speakers",
|
|
203
543
|
description: "Speaker search, profile, or discourse",
|
|
@@ -219,13 +559,13 @@ const COMMANDS = {
|
|
|
219
559
|
if (isId) {
|
|
220
560
|
const sid = Number(identity);
|
|
221
561
|
if (f.segments) {
|
|
222
|
-
const params = { speaker_id: sid, sort: "chronological", days, limit, offset,
|
|
562
|
+
const params = { speaker_id: sid, sort: "chronological", days, limit, offset, channel: channelName };
|
|
223
563
|
if (json)
|
|
224
564
|
return printJson(await client.getSpeakerSegments({ identity_id: sid, days, limit, offset }));
|
|
225
565
|
await runHandler("search_quotes", params);
|
|
226
566
|
}
|
|
227
567
|
else if (topic) {
|
|
228
|
-
const params = { speaker_id: sid, topic, days, limit, offset,
|
|
568
|
+
const params = { speaker_id: sid, topic, days, limit, offset, channel: channelName };
|
|
229
569
|
if (json)
|
|
230
570
|
return printJson(await client.searchSpeakerSegments({ identity_id: sid, query: topic, days, limit, offset }));
|
|
231
571
|
await runHandler("search_quotes", params);
|
|
@@ -239,7 +579,7 @@ const COMMANDS = {
|
|
|
239
579
|
}
|
|
240
580
|
// Segments by name
|
|
241
581
|
if (f.segments) {
|
|
242
|
-
const params = {
|
|
582
|
+
const params = { speaker: identity, sort: "chronological", days, limit, offset, channel: channelName };
|
|
243
583
|
if (json) {
|
|
244
584
|
const byName = await client.getSpeakerByName(identity);
|
|
245
585
|
const sid = byName.speaker_id ?? byName.id;
|
|
@@ -252,9 +592,9 @@ const COMMANDS = {
|
|
|
252
592
|
}
|
|
253
593
|
// Discourse search (name + topic)
|
|
254
594
|
if (topic) {
|
|
255
|
-
const params = {
|
|
595
|
+
const params = { speaker: identity, topic, days, limit, offset, channel: channelName };
|
|
256
596
|
if (json)
|
|
257
|
-
return printJson(await client.searchQuotes({ topic, speaker_name: identity,
|
|
597
|
+
return printJson(await client.searchQuotes({ topic, speaker_name: identity, channel_names: channelName ? [channelName] : undefined, limit, offset }));
|
|
258
598
|
await runHandler("search_quotes", params);
|
|
259
599
|
return;
|
|
260
600
|
}
|
|
@@ -291,12 +631,12 @@ const COMMANDS = {
|
|
|
291
631
|
const offset = Number(f.offset) || 0;
|
|
292
632
|
const dr = daysRange(days);
|
|
293
633
|
const params = {
|
|
294
|
-
|
|
634
|
+
channel: channelName, topic: undefined, sort: "chronological",
|
|
295
635
|
days, ...dr, limit, offset,
|
|
296
636
|
};
|
|
297
637
|
if (json)
|
|
298
638
|
return printJson(await client.searchQuotes({
|
|
299
|
-
|
|
639
|
+
channel_names: channelName ? [channelName] : undefined, start_date: dr.start_date, end_date: dr.end_date,
|
|
300
640
|
limit, offset,
|
|
301
641
|
}));
|
|
302
642
|
await runHandler("search_quotes", params);
|
|
@@ -304,7 +644,7 @@ const COMMANDS = {
|
|
|
304
644
|
}
|
|
305
645
|
if (json)
|
|
306
646
|
return printJson(isId ? await client.getChannel(Number(id)) : await client.getChannelByName(id));
|
|
307
|
-
await runHandler("get_channel", isId ? { channel_id: Number(id) } : {
|
|
647
|
+
await runHandler("get_channel", isId ? { channel_id: Number(id) } : { channel: id });
|
|
308
648
|
},
|
|
309
649
|
},
|
|
310
650
|
episode: {
|
|
@@ -336,9 +676,10 @@ const COMMANDS = {
|
|
|
336
676
|
print("Error: segment ID required.");
|
|
337
677
|
process.exit(1);
|
|
338
678
|
}
|
|
679
|
+
const segId = parseSegmentId(id);
|
|
339
680
|
if (json)
|
|
340
|
-
return printJson(await client.getSegment(
|
|
341
|
-
await runHandler("get_segment", { segment_id:
|
|
681
|
+
return printJson(await client.getSegment(segId));
|
|
682
|
+
await runHandler("get_segment", { segment_id: segId });
|
|
342
683
|
},
|
|
343
684
|
},
|
|
344
685
|
thread: {
|
|
@@ -351,10 +692,11 @@ const COMMANDS = {
|
|
|
351
692
|
print("Error: segment ID required.");
|
|
352
693
|
process.exit(1);
|
|
353
694
|
}
|
|
695
|
+
const segId = parseSegmentId(id);
|
|
354
696
|
const n = Number(args.flags.n) || 5;
|
|
355
697
|
if (json)
|
|
356
|
-
return printJson(await client.getSegmentThread(
|
|
357
|
-
await runHandler("get_thread", { segment_id:
|
|
698
|
+
return printJson(await client.getSegmentThread(segId, n));
|
|
699
|
+
await runHandler("get_thread", { segment_id: segId, n });
|
|
358
700
|
},
|
|
359
701
|
},
|
|
360
702
|
analyze: {
|
|
@@ -371,10 +713,11 @@ const COMMANDS = {
|
|
|
371
713
|
}
|
|
372
714
|
const f = args.flags;
|
|
373
715
|
process.stderr.write("Analyzing... (this may take up to 60 seconds)\n");
|
|
716
|
+
const channelArg = f.channel || undefined;
|
|
374
717
|
const params = {
|
|
375
718
|
query, workflow: f.workflow,
|
|
376
|
-
|
|
377
|
-
|
|
719
|
+
speaker: f.speaker || undefined,
|
|
720
|
+
channels: channelArg?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
378
721
|
language: f.language || undefined,
|
|
379
722
|
country: f.country || undefined,
|
|
380
723
|
days: Number(f.days) || undefined,
|
|
@@ -385,6 +728,35 @@ const COMMANDS = {
|
|
|
385
728
|
await runHandler("analyze_topic", params);
|
|
386
729
|
},
|
|
387
730
|
},
|
|
731
|
+
frames: {
|
|
732
|
+
tool: "compare_frames",
|
|
733
|
+
description: "Compare left vs right media framing on a topic",
|
|
734
|
+
specs: [FLAG_DAYS, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_SKIP_CACHE, FLAG_JSON],
|
|
735
|
+
run: async (args, json) => {
|
|
736
|
+
const [topic] = args.positionals;
|
|
737
|
+
if (!topic) {
|
|
738
|
+
print("Error: topic is required.");
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
const f = args.flags;
|
|
742
|
+
process.stderr.write("Comparing left vs right framing... (this may take up to 60 seconds)\n");
|
|
743
|
+
const params = {
|
|
744
|
+
topic, days: Number(f.days) || 30,
|
|
745
|
+
language: f.language || undefined,
|
|
746
|
+
country: f.country || undefined,
|
|
747
|
+
skip_cache: f["skip-cache"],
|
|
748
|
+
};
|
|
749
|
+
if (json) {
|
|
750
|
+
const shared = { query: topic, workflow: "deep_analysis", days: params.days, language: params.language, country: params.country, skip_cache: params.skip_cache };
|
|
751
|
+
const [left, right] = await Promise.all([
|
|
752
|
+
client.analyze({ ...shared, political_lean_min: 0.0, political_lean_max: 0.35 }),
|
|
753
|
+
client.analyze({ ...shared, political_lean_min: 0.65, political_lean_max: 1.0 }),
|
|
754
|
+
]);
|
|
755
|
+
return printJson({ left, right });
|
|
756
|
+
}
|
|
757
|
+
await runHandler("compare_frames", params);
|
|
758
|
+
},
|
|
759
|
+
},
|
|
388
760
|
compare: {
|
|
389
761
|
tool: "compare_speakers",
|
|
390
762
|
description: "Compare speakers on a topic",
|
|
@@ -435,7 +807,7 @@ const COMMANDS = {
|
|
|
435
807
|
}
|
|
436
808
|
process.stderr.write("Building dossier... (this may take up to 60 seconds)\n");
|
|
437
809
|
const params = {
|
|
438
|
-
topic,
|
|
810
|
+
topic, speaker: speaker, days: Number(args.flags.days) || 180, skip_cache: args.flags["skip-cache"],
|
|
439
811
|
};
|
|
440
812
|
if (json)
|
|
441
813
|
return printJson(await client.analyze({ query: topic, workflow: "speaker_dossier", speaker_name: speaker, days: params.days, skip_cache: params.skip_cache }));
|
|
@@ -459,7 +831,7 @@ const COMMANDS = {
|
|
|
459
831
|
}
|
|
460
832
|
process.stderr.write("Checking contradictions...\n");
|
|
461
833
|
const params = {
|
|
462
|
-
topic,
|
|
834
|
+
topic, speaker: speaker, days: Number(args.flags.days) || 365, skip_cache: args.flags["skip-cache"],
|
|
463
835
|
};
|
|
464
836
|
if (json)
|
|
465
837
|
return printJson(await client.analyze({ query: topic, workflow: "contradiction_check", speaker_name: speaker, days: params.days, skip_cache: params.skip_cache }));
|
|
@@ -537,15 +909,18 @@ const COMMANDS = {
|
|
|
537
909
|
topics: {
|
|
538
910
|
tool: "get_topics",
|
|
539
911
|
description: "Trending topic clusters",
|
|
540
|
-
specs: [FLAG_LANGUAGE, FLAG_LIMIT, { name: "query", alias: "q", type: "string" },
|
|
912
|
+
specs: [FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_LIMIT, FLAG_DAYS, { name: "query", alias: "q", type: "string" },
|
|
541
913
|
{ name: "min-segments", type: "number", default: 50 },
|
|
542
914
|
{ name: "start", type: "string" }, { name: "end", type: "string" }, FLAG_JSON],
|
|
543
915
|
run: async (args, json) => {
|
|
544
916
|
const f = args.flags;
|
|
917
|
+
const dr = f.days ? daysRange(Number(f.days)) : undefined;
|
|
545
918
|
const params = {
|
|
546
919
|
language: f.language || "en", limit: Number(f.limit) || 20,
|
|
547
|
-
query: f.query || undefined,
|
|
548
|
-
|
|
920
|
+
query: f.query || undefined, country: f.country || undefined,
|
|
921
|
+
min_segments: Number(f["min-segments"]) || 50,
|
|
922
|
+
start_date: f.start || dr?.start_date || undefined,
|
|
923
|
+
end_date: f.end || dr?.end_date || undefined,
|
|
549
924
|
};
|
|
550
925
|
if (json)
|
|
551
926
|
return printJson(await client.getTopics(params));
|
|
@@ -643,6 +1018,8 @@ const COMMANDS = {
|
|
|
643
1018
|
},
|
|
644
1019
|
},
|
|
645
1020
|
};
|
|
1021
|
+
// The backend's whoami guide advertises "quotes" — keep it working as an alias.
|
|
1022
|
+
COMMANDS.quotes = COMMANDS.search;
|
|
646
1023
|
// ---------------------------------------------------------------------------
|
|
647
1024
|
// Setup command
|
|
648
1025
|
// ---------------------------------------------------------------------------
|