@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.
Files changed (50) hide show
  1. package/README.md +145 -17
  2. package/bin/s4.js +0 -0
  3. package/bin/signal4-mcp.js +0 -0
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +412 -36
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client.d.ts +114 -5
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +162 -6
  10. package/dist/client.js.map +1 -1
  11. package/dist/format.d.ts +23 -1
  12. package/dist/format.d.ts.map +1 -1
  13. package/dist/format.js +454 -8
  14. package/dist/format.js.map +1 -1
  15. package/dist/handlers/ads.d.ts.map +1 -1
  16. package/dist/handlers/ads.js +20 -2
  17. package/dist/handlers/ads.js.map +1 -1
  18. package/dist/handlers/analyze.d.ts +1 -0
  19. package/dist/handlers/analyze.d.ts.map +1 -1
  20. package/dist/handlers/analyze.js +28 -6
  21. package/dist/handlers/analyze.js.map +1 -1
  22. package/dist/handlers/claim_detail.d.ts +4 -0
  23. package/dist/handlers/claim_detail.d.ts.map +1 -0
  24. package/dist/handlers/claim_detail.js +29 -0
  25. package/dist/handlers/claim_detail.js.map +1 -0
  26. package/dist/handlers/claims.d.ts +12 -1
  27. package/dist/handlers/claims.d.ts.map +1 -1
  28. package/dist/handlers/claims.js +189 -12
  29. package/dist/handlers/claims.js.map +1 -1
  30. package/dist/handlers/explore.d.ts.map +1 -1
  31. package/dist/handlers/explore.js +103 -20
  32. package/dist/handlers/explore.js.map +1 -1
  33. package/dist/handlers/registry.d.ts.map +1 -1
  34. package/dist/handlers/registry.js +18 -2
  35. package/dist/handlers/registry.js.map +1 -1
  36. package/dist/handlers/reports.d.ts.map +1 -1
  37. package/dist/handlers/reports.js +69 -10
  38. package/dist/handlers/reports.js.map +1 -1
  39. package/dist/handlers/search.d.ts.map +1 -1
  40. package/dist/handlers/search.js +24 -10
  41. package/dist/handlers/search.js.map +1 -1
  42. package/dist/handlers/timeline.d.ts +4 -0
  43. package/dist/handlers/timeline.d.ts.map +1 -0
  44. package/dist/handlers/timeline.js +28 -0
  45. package/dist/handlers/timeline.js.map +1 -0
  46. package/dist/index.js +9 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/types.d.ts +251 -0
  49. package/dist/types.d.ts.map +1 -1
  50. 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
- for (const s of specs)
14
- flags[s.name] = s.default ?? (s.type === "boolean" ? false : s.type === "number" ? 0 : "");
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, speaker_name: f.speaker || undefined,
122
+ topic, speaker: f.speaker || undefined,
108
123
  speaker_id: Number(f["speaker-id"]) || undefined,
109
- channel_name: f.channel || undefined,
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.speaker_name,
120
- speaker_identity_id: params.speaker_id, channel_name: params.channel_name,
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
- speaker_name: f.speaker || undefined,
150
- channel_name: f.channel || undefined,
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.speaker_name,
163
- channel_names: params.channel_name ? [params.channel_name] : undefined,
164
- primary_topic: params.category, stance: params.stance, language: params.language,
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
- start_date: params.start_date, end_date: params.end_date,
167
- days: params.days, mode: params.mode, sort: params.sort,
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, speaker_name: f.speaker || undefined,
190
- channel_name: f.channel || undefined,
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(params));
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, channel_name: channelName };
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, channel_name: channelName };
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 = { speaker_name: identity, sort: "chronological", days, limit, offset, channel_name: channelName };
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 = { speaker_name: identity, topic, days, limit, offset, channel_name: channelName };
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, channel_name: channelName, limit, offset }));
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
- channel_name: channelName, topic: undefined, sort: "chronological",
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
- channel_name: channelName, start_date: dr.start_date, end_date: dr.end_date,
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) } : { channel_name: 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(Number(id)));
341
- await runHandler("get_segment", { segment_id: Number(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(Number(id), n));
357
- await runHandler("get_thread", { segment_id: Number(id), n });
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
- speaker_name: f.speaker || undefined,
377
- channel: f.channel || undefined,
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, speaker_name: speaker, days: Number(args.flags.days) || 180, skip_cache: args.flags["skip-cache"],
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, speaker_name: speaker, days: Number(args.flags.days) || 365, skip_cache: args.flags["skip-cache"],
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, min_segments: Number(f["min-segments"]) || 50,
548
- start_date: f.start || undefined, end_date: f.end || undefined,
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
  // ---------------------------------------------------------------------------