@signal4/s4 2.2.0 → 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 +412 -36
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +114 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +162 -6
- 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 +24 -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) {
|
|
@@ -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,8 +131,9 @@ 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,
|
|
@@ -135,6 +151,9 @@ const COMMANDS = {
|
|
|
135
151
|
FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_MODE, FLAG_KEYWORD,
|
|
136
152
|
{ name: "sort", type: "string", default: "relevance" },
|
|
137
153
|
{ name: "min-obs", type: "number", default: 1 },
|
|
154
|
+
{ name: "min-similarity", type: "number" },
|
|
155
|
+
{ name: "political-lean-min", type: "number" }, { name: "political-lean-max", type: "number" },
|
|
156
|
+
{ name: "source", type: "string" },
|
|
138
157
|
FLAG_DAYS, FLAG_LIMIT, FLAG_OFFSET, FLAG_START, FLAG_END, FLAG_JSON],
|
|
139
158
|
run: async (args, json) => {
|
|
140
159
|
const [positionalTopic] = args.positionals;
|
|
@@ -144,33 +163,319 @@ const COMMANDS = {
|
|
|
144
163
|
const mode = keyword ? "keyword" : f.mode;
|
|
145
164
|
const days = Number(f.days) || 30;
|
|
146
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;
|
|
147
169
|
const params = {
|
|
148
170
|
topic, entity: f.entity || undefined,
|
|
149
|
-
|
|
150
|
-
|
|
171
|
+
speaker: f.speaker || undefined,
|
|
172
|
+
channel: f.channel || undefined,
|
|
151
173
|
category: f.category || undefined,
|
|
152
174
|
stance: f.stance || undefined,
|
|
153
175
|
language: f.language || undefined,
|
|
154
176
|
country: f.country || undefined,
|
|
177
|
+
political_lean_min: leanMin, political_lean_max: leanMax,
|
|
178
|
+
min_similarity: minSim,
|
|
179
|
+
source: f.source || undefined,
|
|
155
180
|
days, mode,
|
|
156
181
|
sort: f.sort, min_observations: Number(f["min-obs"]) || 1,
|
|
157
182
|
limit: Number(f.limit), offset: Number(f.offset),
|
|
158
183
|
...dr,
|
|
159
184
|
};
|
|
185
|
+
const claimsDr = dr ?? daysRange(days);
|
|
160
186
|
if (json)
|
|
161
187
|
return printJson(await client.searchClaims({
|
|
162
|
-
topic: params.topic, entity: params.entity, speaker_name: params.
|
|
163
|
-
channel_names: params.
|
|
164
|
-
|
|
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,
|
|
165
191
|
country: params.country,
|
|
166
|
-
|
|
167
|
-
|
|
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,
|
|
168
196
|
min_observations: params.min_observations > 1 ? params.min_observations : undefined,
|
|
169
197
|
limit: params.limit, offset: params.offset,
|
|
170
198
|
}));
|
|
171
199
|
await runHandler("search_claims", params);
|
|
172
200
|
},
|
|
173
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
|
+
},
|
|
174
479
|
trend: {
|
|
175
480
|
tool: "detect_trend",
|
|
176
481
|
description: "Detect topic volume trends",
|
|
@@ -185,19 +490,53 @@ const COMMANDS = {
|
|
|
185
490
|
process.exit(1);
|
|
186
491
|
}
|
|
187
492
|
const f = args.flags;
|
|
493
|
+
const trendChannel = f.channel || undefined;
|
|
188
494
|
const params = {
|
|
189
|
-
topic,
|
|
190
|
-
|
|
495
|
+
topic, speaker: f.speaker || undefined,
|
|
496
|
+
channel: trendChannel,
|
|
191
497
|
language: f.language || undefined,
|
|
192
498
|
country: f.country || undefined,
|
|
193
499
|
recent_days: Number(f.recent) || 7, baseline_days: Number(f.baseline) || 30,
|
|
194
500
|
min_similarity: Number(f["min-similarity"]) || undefined,
|
|
195
501
|
};
|
|
196
502
|
if (json)
|
|
197
|
-
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
|
+
}));
|
|
198
507
|
await runHandler("detect_trend", params);
|
|
199
508
|
},
|
|
200
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
|
+
},
|
|
201
540
|
speaker: {
|
|
202
541
|
tool: "find_speakers",
|
|
203
542
|
description: "Speaker search, profile, or discourse",
|
|
@@ -219,13 +558,13 @@ const COMMANDS = {
|
|
|
219
558
|
if (isId) {
|
|
220
559
|
const sid = Number(identity);
|
|
221
560
|
if (f.segments) {
|
|
222
|
-
const params = { speaker_id: sid, sort: "chronological", days, limit, offset,
|
|
561
|
+
const params = { speaker_id: sid, sort: "chronological", days, limit, offset, channel: channelName };
|
|
223
562
|
if (json)
|
|
224
563
|
return printJson(await client.getSpeakerSegments({ identity_id: sid, days, limit, offset }));
|
|
225
564
|
await runHandler("search_quotes", params);
|
|
226
565
|
}
|
|
227
566
|
else if (topic) {
|
|
228
|
-
const params = { speaker_id: sid, topic, days, limit, offset,
|
|
567
|
+
const params = { speaker_id: sid, topic, days, limit, offset, channel: channelName };
|
|
229
568
|
if (json)
|
|
230
569
|
return printJson(await client.searchSpeakerSegments({ identity_id: sid, query: topic, days, limit, offset }));
|
|
231
570
|
await runHandler("search_quotes", params);
|
|
@@ -239,7 +578,7 @@ const COMMANDS = {
|
|
|
239
578
|
}
|
|
240
579
|
// Segments by name
|
|
241
580
|
if (f.segments) {
|
|
242
|
-
const params = {
|
|
581
|
+
const params = { speaker: identity, sort: "chronological", days, limit, offset, channel: channelName };
|
|
243
582
|
if (json) {
|
|
244
583
|
const byName = await client.getSpeakerByName(identity);
|
|
245
584
|
const sid = byName.speaker_id ?? byName.id;
|
|
@@ -252,9 +591,9 @@ const COMMANDS = {
|
|
|
252
591
|
}
|
|
253
592
|
// Discourse search (name + topic)
|
|
254
593
|
if (topic) {
|
|
255
|
-
const params = {
|
|
594
|
+
const params = { speaker: identity, topic, days, limit, offset, channel: channelName };
|
|
256
595
|
if (json)
|
|
257
|
-
return printJson(await client.searchQuotes({ topic, speaker_name: identity,
|
|
596
|
+
return printJson(await client.searchQuotes({ topic, speaker_name: identity, channel_names: channelName ? [channelName] : undefined, limit, offset }));
|
|
258
597
|
await runHandler("search_quotes", params);
|
|
259
598
|
return;
|
|
260
599
|
}
|
|
@@ -291,12 +630,12 @@ const COMMANDS = {
|
|
|
291
630
|
const offset = Number(f.offset) || 0;
|
|
292
631
|
const dr = daysRange(days);
|
|
293
632
|
const params = {
|
|
294
|
-
|
|
633
|
+
channel: channelName, topic: undefined, sort: "chronological",
|
|
295
634
|
days, ...dr, limit, offset,
|
|
296
635
|
};
|
|
297
636
|
if (json)
|
|
298
637
|
return printJson(await client.searchQuotes({
|
|
299
|
-
|
|
638
|
+
channel_names: channelName ? [channelName] : undefined, start_date: dr.start_date, end_date: dr.end_date,
|
|
300
639
|
limit, offset,
|
|
301
640
|
}));
|
|
302
641
|
await runHandler("search_quotes", params);
|
|
@@ -304,7 +643,7 @@ const COMMANDS = {
|
|
|
304
643
|
}
|
|
305
644
|
if (json)
|
|
306
645
|
return printJson(isId ? await client.getChannel(Number(id)) : await client.getChannelByName(id));
|
|
307
|
-
await runHandler("get_channel", isId ? { channel_id: Number(id) } : {
|
|
646
|
+
await runHandler("get_channel", isId ? { channel_id: Number(id) } : { channel: id });
|
|
308
647
|
},
|
|
309
648
|
},
|
|
310
649
|
episode: {
|
|
@@ -336,9 +675,10 @@ const COMMANDS = {
|
|
|
336
675
|
print("Error: segment ID required.");
|
|
337
676
|
process.exit(1);
|
|
338
677
|
}
|
|
678
|
+
const segId = parseSegmentId(id);
|
|
339
679
|
if (json)
|
|
340
|
-
return printJson(await client.getSegment(
|
|
341
|
-
await runHandler("get_segment", { segment_id:
|
|
680
|
+
return printJson(await client.getSegment(segId));
|
|
681
|
+
await runHandler("get_segment", { segment_id: segId });
|
|
342
682
|
},
|
|
343
683
|
},
|
|
344
684
|
thread: {
|
|
@@ -351,10 +691,11 @@ const COMMANDS = {
|
|
|
351
691
|
print("Error: segment ID required.");
|
|
352
692
|
process.exit(1);
|
|
353
693
|
}
|
|
694
|
+
const segId = parseSegmentId(id);
|
|
354
695
|
const n = Number(args.flags.n) || 5;
|
|
355
696
|
if (json)
|
|
356
|
-
return printJson(await client.getSegmentThread(
|
|
357
|
-
await runHandler("get_thread", { segment_id:
|
|
697
|
+
return printJson(await client.getSegmentThread(segId, n));
|
|
698
|
+
await runHandler("get_thread", { segment_id: segId, n });
|
|
358
699
|
},
|
|
359
700
|
},
|
|
360
701
|
analyze: {
|
|
@@ -371,10 +712,11 @@ const COMMANDS = {
|
|
|
371
712
|
}
|
|
372
713
|
const f = args.flags;
|
|
373
714
|
process.stderr.write("Analyzing... (this may take up to 60 seconds)\n");
|
|
715
|
+
const channelArg = f.channel || undefined;
|
|
374
716
|
const params = {
|
|
375
717
|
query, workflow: f.workflow,
|
|
376
|
-
|
|
377
|
-
|
|
718
|
+
speaker: f.speaker || undefined,
|
|
719
|
+
channels: channelArg?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
378
720
|
language: f.language || undefined,
|
|
379
721
|
country: f.country || undefined,
|
|
380
722
|
days: Number(f.days) || undefined,
|
|
@@ -385,6 +727,35 @@ const COMMANDS = {
|
|
|
385
727
|
await runHandler("analyze_topic", params);
|
|
386
728
|
},
|
|
387
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
|
+
},
|
|
388
759
|
compare: {
|
|
389
760
|
tool: "compare_speakers",
|
|
390
761
|
description: "Compare speakers on a topic",
|
|
@@ -435,7 +806,7 @@ const COMMANDS = {
|
|
|
435
806
|
}
|
|
436
807
|
process.stderr.write("Building dossier... (this may take up to 60 seconds)\n");
|
|
437
808
|
const params = {
|
|
438
|
-
topic,
|
|
809
|
+
topic, speaker: speaker, days: Number(args.flags.days) || 180, skip_cache: args.flags["skip-cache"],
|
|
439
810
|
};
|
|
440
811
|
if (json)
|
|
441
812
|
return printJson(await client.analyze({ query: topic, workflow: "speaker_dossier", speaker_name: speaker, days: params.days, skip_cache: params.skip_cache }));
|
|
@@ -459,7 +830,7 @@ const COMMANDS = {
|
|
|
459
830
|
}
|
|
460
831
|
process.stderr.write("Checking contradictions...\n");
|
|
461
832
|
const params = {
|
|
462
|
-
topic,
|
|
833
|
+
topic, speaker: speaker, days: Number(args.flags.days) || 365, skip_cache: args.flags["skip-cache"],
|
|
463
834
|
};
|
|
464
835
|
if (json)
|
|
465
836
|
return printJson(await client.analyze({ query: topic, workflow: "contradiction_check", speaker_name: speaker, days: params.days, skip_cache: params.skip_cache }));
|
|
@@ -537,15 +908,18 @@ const COMMANDS = {
|
|
|
537
908
|
topics: {
|
|
538
909
|
tool: "get_topics",
|
|
539
910
|
description: "Trending topic clusters",
|
|
540
|
-
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" },
|
|
541
912
|
{ name: "min-segments", type: "number", default: 50 },
|
|
542
913
|
{ name: "start", type: "string" }, { name: "end", type: "string" }, FLAG_JSON],
|
|
543
914
|
run: async (args, json) => {
|
|
544
915
|
const f = args.flags;
|
|
916
|
+
const dr = f.days ? daysRange(Number(f.days)) : undefined;
|
|
545
917
|
const params = {
|
|
546
918
|
language: f.language || "en", limit: Number(f.limit) || 20,
|
|
547
|
-
query: f.query || undefined,
|
|
548
|
-
|
|
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,
|
|
549
923
|
};
|
|
550
924
|
if (json)
|
|
551
925
|
return printJson(await client.getTopics(params));
|
|
@@ -643,6 +1017,8 @@ const COMMANDS = {
|
|
|
643
1017
|
},
|
|
644
1018
|
},
|
|
645
1019
|
};
|
|
1020
|
+
// The backend's whoami guide advertises "quotes" — keep it working as an alias.
|
|
1021
|
+
COMMANDS.quotes = COMMANDS.search;
|
|
646
1022
|
// ---------------------------------------------------------------------------
|
|
647
1023
|
// Setup command
|
|
648
1024
|
// ---------------------------------------------------------------------------
|