@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.
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 +414 -37
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client.d.ts +151 -5
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +214 -8
  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 +29 -12
  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 +336 -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,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.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,
124
- limit: params.count_only ? 1 : params.limit, offset: params.offset,
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
- speaker_name: f.speaker || undefined,
150
- channel_name: f.channel || undefined,
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.speaker_name,
163
- channel_names: params.channel_name ? [params.channel_name] : undefined,
164
- primary_topic: params.category, stance: params.stance, language: params.language,
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
- start_date: params.start_date, end_date: params.end_date,
167
- days: params.days, mode: params.mode, sort: params.sort,
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, speaker_name: f.speaker || undefined,
190
- channel_name: f.channel || undefined,
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(params));
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, channel_name: channelName };
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, channel_name: channelName };
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 = { speaker_name: identity, sort: "chronological", days, limit, offset, channel_name: channelName };
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 = { speaker_name: identity, topic, days, limit, offset, channel_name: channelName };
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, channel_name: channelName, limit, offset }));
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
- channel_name: channelName, topic: undefined, sort: "chronological",
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
- channel_name: channelName, start_date: dr.start_date, end_date: dr.end_date,
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) } : { channel_name: 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(Number(id)));
341
- await runHandler("get_segment", { segment_id: Number(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(Number(id), n));
357
- await runHandler("get_thread", { segment_id: Number(id), n });
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
- speaker_name: f.speaker || undefined,
377
- channel: f.channel || undefined,
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, speaker_name: speaker, days: Number(args.flags.days) || 180, skip_cache: args.flags["skip-cache"],
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, speaker_name: speaker, days: Number(args.flags.days) || 365, skip_cache: args.flags["skip-cache"],
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, min_segments: Number(f["min-segments"]) || 50,
548
- start_date: f.start || undefined, end_date: f.end || undefined,
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
  // ---------------------------------------------------------------------------