@signal4/s4 2.0.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 (64) hide show
  1. package/README.md +92 -0
  2. package/bin/s4.js +7 -0
  3. package/bin/signal4-mcp.js +2 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +737 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/client.d.ts +194 -0
  9. package/dist/client.d.ts.map +1 -0
  10. package/dist/client.js +397 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/config.d.ts +4 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +37 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/format.d.ts +60 -0
  17. package/dist/format.d.ts.map +1 -0
  18. package/dist/format.js +682 -0
  19. package/dist/format.js.map +1 -0
  20. package/dist/handler_types.d.ts +11 -0
  21. package/dist/handler_types.d.ts.map +1 -0
  22. package/dist/handler_types.js +3 -0
  23. package/dist/handler_types.js.map +1 -0
  24. package/dist/handlers/ads.d.ts +4 -0
  25. package/dist/handlers/ads.d.ts.map +1 -0
  26. package/dist/handlers/ads.js +42 -0
  27. package/dist/handlers/ads.js.map +1 -0
  28. package/dist/handlers/analyze.d.ts +8 -0
  29. package/dist/handlers/analyze.d.ts.map +1 -0
  30. package/dist/handlers/analyze.js +77 -0
  31. package/dist/handlers/analyze.js.map +1 -0
  32. package/dist/handlers/bookmarks.d.ts +4 -0
  33. package/dist/handlers/bookmarks.d.ts.map +1 -0
  34. package/dist/handlers/bookmarks.js +42 -0
  35. package/dist/handlers/bookmarks.js.map +1 -0
  36. package/dist/handlers/claims.d.ts +4 -0
  37. package/dist/handlers/claims.d.ts.map +1 -0
  38. package/dist/handlers/claims.js +47 -0
  39. package/dist/handlers/claims.js.map +1 -0
  40. package/dist/handlers/explore.d.ts +9 -0
  41. package/dist/handlers/explore.d.ts.map +1 -0
  42. package/dist/handlers/explore.js +120 -0
  43. package/dist/handlers/explore.js.map +1 -0
  44. package/dist/handlers/registry.d.ts +4 -0
  45. package/dist/handlers/registry.d.ts.map +1 -0
  46. package/dist/handlers/registry.js +33 -0
  47. package/dist/handlers/registry.js.map +1 -0
  48. package/dist/handlers/reports.d.ts +9 -0
  49. package/dist/handlers/reports.d.ts.map +1 -0
  50. package/dist/handlers/reports.js +160 -0
  51. package/dist/handlers/reports.js.map +1 -0
  52. package/dist/handlers/search.d.ts +5 -0
  53. package/dist/handlers/search.d.ts.map +1 -0
  54. package/dist/handlers/search.js +128 -0
  55. package/dist/handlers/search.js.map +1 -0
  56. package/dist/index.d.ts +8 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +97 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/types.d.ts +482 -0
  61. package/dist/types.d.ts.map +1 -0
  62. package/dist/types.js +3 -0
  63. package/dist/types.js.map +1 -0
  64. package/package.json +52 -0
package/dist/format.js ADDED
@@ -0,0 +1,682 @@
1
+ /** Shared formatting utilities — markdown output for MCP tools. */
2
+ const PUBLIC_URL = "https://api.signal4.ca";
3
+ // ---------------------------------------------------------------------------
4
+ // Primitives
5
+ // ---------------------------------------------------------------------------
6
+ export function fmtTime(seconds) {
7
+ if (seconds == null)
8
+ return "0:00";
9
+ const s = Math.floor(seconds);
10
+ const h = Math.floor(s / 3600);
11
+ const m = Math.floor((s % 3600) / 60);
12
+ const sec = s % 60;
13
+ if (h)
14
+ return `${h}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
15
+ return `${m}:${String(sec).padStart(2, "0")}`;
16
+ }
17
+ export function fmtHours(seconds) {
18
+ if (!seconds)
19
+ return "?";
20
+ const h = seconds / 3600;
21
+ if (h >= 1)
22
+ return `${h.toLocaleString("en", { maximumFractionDigits: 1 })}h`;
23
+ return fmtTime(seconds);
24
+ }
25
+ export function fmtDuration(seconds) {
26
+ if (!seconds)
27
+ return "?";
28
+ const m = Math.floor(seconds / 60);
29
+ const s = Math.floor(seconds % 60);
30
+ if (m >= 60) {
31
+ const h = Math.floor(m / 60);
32
+ const rm = m % 60;
33
+ return `${h}h${String(rm).padStart(2, "0")}m`;
34
+ }
35
+ return `${m}m${String(s).padStart(2, "0")}s`;
36
+ }
37
+ export function truncate(text, maxlen = 200) {
38
+ if (!text)
39
+ return "";
40
+ const clean = text.replace(/\n/g, " ").trim();
41
+ if (clean.length <= maxlen)
42
+ return clean;
43
+ const cut = clean.slice(0, maxlen);
44
+ const last = cut.lastIndexOf(" ");
45
+ return (last > maxlen * 0.5 ? cut.slice(0, last) : cut) + "...";
46
+ }
47
+ export function fmtDate(iso) {
48
+ return iso ? iso.slice(0, 10) : "?";
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // Quote dedup
52
+ // ---------------------------------------------------------------------------
53
+ export function dedupQuotes(quotes) {
54
+ const seen = new Set();
55
+ const out = [];
56
+ for (const q of quotes) {
57
+ const key = [
58
+ q.episode_title ?? q.content_id ?? "",
59
+ q.timestamp_start ?? q.start_time ?? 0,
60
+ (q.text ?? "").slice(0, 80),
61
+ ].join("|");
62
+ if (!seen.has(key)) {
63
+ seen.add(key);
64
+ out.push(q);
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // Similarity tier
71
+ // ---------------------------------------------------------------------------
72
+ function simTier(sim) {
73
+ if (sim >= 0.60)
74
+ return "strong";
75
+ if (sim >= 0.45)
76
+ return "relevant";
77
+ if (sim >= 0.40)
78
+ return "borderline";
79
+ if (sim > 0)
80
+ return "weak";
81
+ return "";
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // Clip URL
85
+ // ---------------------------------------------------------------------------
86
+ function clipUrl(contentId, start, end, existing) {
87
+ if (existing && !existing.includes("localhost"))
88
+ return existing;
89
+ if (contentId && start != null && end != null) {
90
+ return `${PUBLIC_URL}/api/v1/media/content/${contentId}?start_time=${start}&end_time=${end}&media_type=audio`;
91
+ }
92
+ return "";
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // Empty state
96
+ // ---------------------------------------------------------------------------
97
+ export function emptyStateSuggestions(params) {
98
+ const suggestions = [];
99
+ if (params.days && params.days < 30)
100
+ suggestions.push("Try a wider window: days=30");
101
+ else if (params.days && params.days < 90)
102
+ suggestions.push("Try a wider window: days=90");
103
+ if (params.mode === "semantic" && params.topic)
104
+ suggestions.push("Try hybrid mode: mode='hybrid'");
105
+ if (params.mode === "keyword" && params.topic)
106
+ suggestions.push("Try semantic mode: mode='semantic'");
107
+ if (params.speaker_name && params.topic)
108
+ suggestions.push("Drop speaker_name to search all speakers");
109
+ const query = params.topic || params.speaker_name || "this query";
110
+ let msg = `No matching content for "${query}"`;
111
+ if (params.days)
112
+ msg += ` in the last ${params.days} days`;
113
+ msg += ".";
114
+ if (suggestions.length)
115
+ msg += "\n" + suggestions.map((s) => `- ${s}`).join("\n");
116
+ return msg;
117
+ }
118
+ // ---------------------------------------------------------------------------
119
+ // Quotes
120
+ // ---------------------------------------------------------------------------
121
+ export function formatQuotes(data) {
122
+ const quotes = dedupQuotes(data.quotes ?? []);
123
+ if (!quotes.length)
124
+ return "No quotes found matching your search criteria.";
125
+ const total = data.total_results ?? quotes.length;
126
+ const ms = data.processing_time_ms ?? 0;
127
+ const lines = [`**${total} results** (showing ${quotes.length}, ${ms.toFixed(0)}ms)\n`];
128
+ for (let i = 0; i < quotes.length; i++) {
129
+ const q = quotes[i];
130
+ const spk = q.speaker ?? {};
131
+ const spkName = spk.name ?? "Unknown";
132
+ const spkId = spk.identity_id;
133
+ const conf = spk.confidence ?? 0;
134
+ const appear = spk.appearance_type ?? "";
135
+ const sim = q.similarity ?? 0;
136
+ const ch = q.channel_name ?? "?";
137
+ const date = fmtDate(q.episode_date);
138
+ const text = (q.text ?? "").slice(0, 300);
139
+ const segId = q.segment_id ?? "?";
140
+ const contentId = q.content_id ?? "?";
141
+ const tStart = q.timestamp_start ?? q.start_time;
142
+ const tEnd = q.timestamp_end ?? q.end_time;
143
+ const clip = clipUrl(q.content_id, tStart ?? undefined, tEnd ?? undefined, q.clip_url);
144
+ const tier = simTier(sim);
145
+ const tierStr = tier ? ` ${tier}` : "";
146
+ const idStr = spkId ? ` id:${spkId}` : "";
147
+ const clipTag = appear === "clipped" ? " **[clipped]**" : "";
148
+ const timeStr = tStart != null ? ` ${fmtTime(tStart)}-${fmtTime(tEnd)}` : "";
149
+ lines.push(`${i + 1}. **${spkName}**${clipTag} (conf:${Math.round(conf * 100)}%${idStr}) on ${ch} ` +
150
+ `[${date}${timeStr}] sim:${sim.toFixed(2)}${tierStr}`);
151
+ lines.push(` seg:${segId} | episode:${contentId}`);
152
+ if (clip)
153
+ lines.push(` 🔊 ${clip}`);
154
+ lines.push(` > ${text}\n`);
155
+ }
156
+ if (total > quotes.length) {
157
+ lines.push(`_${total - quotes.length} more results available (use offset parameter)_`);
158
+ }
159
+ return lines.join("\n");
160
+ }
161
+ // ---------------------------------------------------------------------------
162
+ // Speaker
163
+ // ---------------------------------------------------------------------------
164
+ export function formatSpeaker(data) {
165
+ const name = data.name ?? data.primary_name ?? "Unknown";
166
+ const lines = [`# ${name}`];
167
+ if (data.occupation) {
168
+ let occ = `**${data.occupation}**`;
169
+ if (data.organization)
170
+ occ += ` at ${data.organization}`;
171
+ lines.push(occ);
172
+ }
173
+ if (data.bio)
174
+ lines.push(`\n${data.bio}`);
175
+ lines.push("");
176
+ const stats = [];
177
+ if (data.total_appearances)
178
+ stats.push(`${data.total_appearances} appearances`);
179
+ if (data.total_episodes)
180
+ stats.push(`${data.total_episodes} episodes`);
181
+ if (data.total_duration_seconds)
182
+ stats.push(`${fmtHours(data.total_duration_seconds)} total`);
183
+ if (stats.length)
184
+ lines.push(`**Stats:** ${stats.join(", ")}`);
185
+ const sid = data.speaker_id ?? data.id;
186
+ if (sid)
187
+ lines.push(`Speaker ID: \`${sid}\``);
188
+ const topChannels = data.top_channels ?? [];
189
+ if (topChannels.length) {
190
+ lines.push("\n**Top Channels:**");
191
+ for (const ch of topChannels.slice(0, 8)) {
192
+ lines.push(`- ${ch.name ?? "?"} (ID: ${ch.channel_id ?? "?"}, ${ch.count ?? 0} appearances)`);
193
+ }
194
+ }
195
+ const related = data.related_speakers ?? [];
196
+ if (related.length) {
197
+ lines.push("\n**Co-appearances:**");
198
+ for (const r of related.slice(0, 10)) {
199
+ const count = r.appearance_count ?? r.co_appearances ?? "?";
200
+ lines.push(`- ${r.name ?? "?"} (${count})`);
201
+ }
202
+ }
203
+ const recent = data.recent_episodes ?? [];
204
+ if (recent.length) {
205
+ lines.push("\n**Recent Episodes:**");
206
+ for (const ep of recent.slice(0, 8)) {
207
+ const date = fmtDate(ep.publish_date);
208
+ lines.push(`- [${date}] ${ep.channel_name ?? "?"} — ${ep.title ?? "?"}`);
209
+ }
210
+ }
211
+ return lines.join("\n");
212
+ }
213
+ // ---------------------------------------------------------------------------
214
+ // Channel
215
+ // ---------------------------------------------------------------------------
216
+ export function formatChannel(data) {
217
+ const name = data.display_name ?? data.name ?? "Channel";
218
+ const lines = [`# ${name}`];
219
+ const meta = [];
220
+ if (data.platform)
221
+ meta.push(`**Platform:** ${data.platform}`);
222
+ if (data.episode_count)
223
+ meta.push(`**Episodes:** ${data.episode_count}`);
224
+ if (data.language)
225
+ meta.push(`**Language:** ${data.language}`);
226
+ if (meta.length)
227
+ lines.push(meta.join(" | "));
228
+ lines.push(`Channel ID: \`${data.channel_id ?? data.id ?? ""}\``);
229
+ if (data.description)
230
+ lines.push(`\n${data.description.slice(0, 500)}`);
231
+ if (data.primary_url)
232
+ lines.push(`\nURL: ${data.primary_url}`);
233
+ const ranking = data.ranking;
234
+ if (ranking) {
235
+ const parts = [];
236
+ if (ranking.tier)
237
+ parts.push(`tier ${ranking.tier}`);
238
+ if (ranking.importance_rank)
239
+ parts.push(`rank #${ranking.importance_rank}`);
240
+ if (parts.length)
241
+ lines.push(`\n**Ranking:** ${parts.join(", ")}`);
242
+ for (const cp of (ranking.chart_positions ?? []).slice(0, 5)) {
243
+ lines.push(`- Chart: ${cp.platform ?? "?"} ${cp.country ?? "?"}/${cp.category ?? "?"} #${cp.rank ?? "?"}`);
244
+ }
245
+ }
246
+ const speakers = data.regular_speakers ?? data.speakers ?? [];
247
+ if (speakers.length) {
248
+ lines.push("\n**Speakers:**");
249
+ for (const s of speakers.slice(0, 10)) {
250
+ const count = s.appearance_count ?? s.count ?? "?";
251
+ lines.push(`- ${s.name ?? "?"} (${count} appearances)`);
252
+ }
253
+ }
254
+ const recent = data.recent_episodes ?? [];
255
+ if (recent.length) {
256
+ lines.push("\n**Recent Episodes:**");
257
+ for (const ep of recent.slice(0, 8)) {
258
+ lines.push(`- [${fmtDate(ep.publish_date)}] ${ep.title ?? "?"}`);
259
+ }
260
+ }
261
+ return lines.join("\n");
262
+ }
263
+ // ---------------------------------------------------------------------------
264
+ // Segment
265
+ // ---------------------------------------------------------------------------
266
+ export function formatSegment(data) {
267
+ const segId = data.segment_id ?? "?";
268
+ const spk = data.speaker_name ?? "Unknown";
269
+ const ch = data.channel_name ?? "?";
270
+ const title = data.episode_title ?? "?";
271
+ const contentId = data.content_id ?? "?";
272
+ const date = fmtDate(data.episode_date);
273
+ const tStart = data.start_time;
274
+ const tEnd = data.end_time;
275
+ const clip = clipUrl(data.content_id, tStart ?? undefined, tEnd ?? undefined, data.clip_url);
276
+ const lines = [
277
+ `**Segment ${segId}**`,
278
+ `- **Speaker:** ${spk}`,
279
+ `- **Channel:** ${ch}`,
280
+ `- **Episode:** ${title} (\`${contentId}\`)`,
281
+ `- **Date:** ${date}`,
282
+ ];
283
+ if (tStart != null)
284
+ lines.push(`- **Time:** ${fmtTime(tStart)} – ${fmtTime(tEnd)}`);
285
+ if (clip)
286
+ lines.push(`- **Audio clip:** ${clip}`);
287
+ lines.push(`\n> ${data.text ?? ""}`);
288
+ return lines.join("\n");
289
+ }
290
+ // ---------------------------------------------------------------------------
291
+ // Analysis
292
+ // ---------------------------------------------------------------------------
293
+ export function formatAnalysis(data) {
294
+ if (data.error)
295
+ return `Error: ${data.error}`;
296
+ const lines = ["**Analysis complete**\n"];
297
+ const q = data.quantitative_metrics ?? {};
298
+ const centrality = q.discourse_centrality;
299
+ if (centrality?.label) {
300
+ lines.push(`Discourse centrality: ${centrality.label} (${centrality.level ?? "?"}/5)`);
301
+ }
302
+ const parts = [];
303
+ if (q.total_segments)
304
+ parts.push(`${q.total_segments} segments`);
305
+ if (q.unique_channels)
306
+ parts.push(`${q.unique_channels} channels`);
307
+ if (q.unique_videos ?? q.episode_count)
308
+ parts.push(`${q.unique_videos ?? q.episode_count} episodes`);
309
+ if (parts.length)
310
+ lines.push(parts.join(", ") + "\n");
311
+ const themes = data.themes ?? [];
312
+ if (themes.length) {
313
+ lines.push("**Themes:**");
314
+ for (let i = 0; i < themes.length; i++) {
315
+ const t = themes[i];
316
+ if (!t)
317
+ continue;
318
+ const name = t.name ?? t.label ?? `Theme ${i + 1}`;
319
+ const count = t.segment_count ?? t.count;
320
+ const countStr = count ? ` (${count} segments)` : "";
321
+ lines.push(`${i + 1}. ${name}${countStr}`);
322
+ const desc = t.summary ?? t.description;
323
+ if (desc)
324
+ lines.push(` ${desc.slice(0, 200)}`);
325
+ }
326
+ lines.push("");
327
+ }
328
+ if (data.summary)
329
+ lines.push(data.summary);
330
+ return lines.join("\n");
331
+ }
332
+ // ---------------------------------------------------------------------------
333
+ // Comparison
334
+ // ---------------------------------------------------------------------------
335
+ export function formatComparison(data, topic) {
336
+ if (data.error)
337
+ return `Error: ${data.error}`;
338
+ const lines = [`**Speaker comparison: ${topic}**\n`];
339
+ const speakers = data.speakers ?? {};
340
+ for (const [name, sp] of Object.entries(speakers)) {
341
+ const count = sp.segment_count ?? 0;
342
+ lines.push(`### ${name} (${count} segments)`);
343
+ if (sp.position_summary)
344
+ lines.push(`**Position:** ${sp.position_summary}`);
345
+ if (sp.framing)
346
+ lines.push(`**Framing:** ${sp.framing}`);
347
+ if (sp.tone)
348
+ lines.push(`**Tone:** ${sp.tone}`);
349
+ const claims = sp.key_claims ?? [];
350
+ if (claims.length) {
351
+ lines.push("**Key claims:**");
352
+ for (const c of claims.slice(0, 5)) {
353
+ const text = typeof c === "string" ? c : c.claim ?? String(c);
354
+ lines.push(`- ${text}`);
355
+ }
356
+ }
357
+ lines.push("");
358
+ }
359
+ if (data.comparison || data.summary) {
360
+ lines.push("---");
361
+ lines.push(data.comparison ?? data.summary ?? "");
362
+ }
363
+ return lines.join("\n");
364
+ }
365
+ // ---------------------------------------------------------------------------
366
+ // Dossier
367
+ // ---------------------------------------------------------------------------
368
+ export function formatDossier(data, speakerName, topic) {
369
+ if (data.error)
370
+ return `Error: ${data.error}`;
371
+ const lines = [`**Dossier: ${speakerName} on ${topic}**\n`];
372
+ const sp = data.speaker ?? {};
373
+ if (sp.occupation)
374
+ lines.push(`*${sp.occupation}*`);
375
+ if (sp.segment_count)
376
+ lines.push(`${sp.segment_count} segments analyzed\n`);
377
+ const contras = data.contradictions;
378
+ if (contras?.verdict) {
379
+ lines.push(`**Contradiction check:** ${contras.verdict.replace(/_/g, " ")}`);
380
+ for (const c of contras.contradictions ?? []) {
381
+ lines.push(`- [${c.severity ?? "?"}] ${c.description ?? "?"}`);
382
+ }
383
+ lines.push("");
384
+ }
385
+ if (data.summary)
386
+ lines.push(data.summary);
387
+ return lines.join("\n");
388
+ }
389
+ // ---------------------------------------------------------------------------
390
+ // Contradictions
391
+ // ---------------------------------------------------------------------------
392
+ export function formatContradictions(data, speakerName, topic) {
393
+ if (data.error)
394
+ return `Error: ${data.error}`;
395
+ const lines = [`**Contradiction check: ${speakerName} on ${topic}**\n`];
396
+ const contras = data.contradictions ?? {};
397
+ const verdict = contras.verdict ?? "unknown";
398
+ lines.push(`**Verdict:** ${verdict.replace(/_/g, " ")}\n`);
399
+ for (const c of contras.contradictions ?? []) {
400
+ lines.push(`### [${c.severity ?? "?"}] ${c.description ?? "?"}`);
401
+ if (c.earlier?.claim)
402
+ lines.push(`**Earlier** (${c.earlier.date ?? "?"}): ${c.earlier.claim}`);
403
+ if (c.later?.claim)
404
+ lines.push(`**Later** (${c.later.date ?? "?"}): ${c.later.claim}`);
405
+ if (c.possible_context)
406
+ lines.push(`*Context:* ${c.possible_context}`);
407
+ lines.push("");
408
+ }
409
+ const consistent = contras.consistent_positions ?? [];
410
+ if (consistent.length) {
411
+ lines.push("**Consistent positions:**");
412
+ for (const pos of consistent)
413
+ lines.push(`- ${pos}`);
414
+ lines.push("");
415
+ }
416
+ const timeline = data.stance_timeline ?? [];
417
+ if (timeline.length) {
418
+ lines.push("**Stance timeline:**");
419
+ for (const t of timeline.slice(0, 20)) {
420
+ lines.push(`- ${t.segment_id ?? "?"}: ${t.stance ?? "?"} — ${(t.claim ?? "").slice(0, 100)}`);
421
+ }
422
+ }
423
+ if (data.summary && !(contras.contradictions ?? []).length)
424
+ lines.push(data.summary);
425
+ return lines.join("\n");
426
+ }
427
+ // ---------------------------------------------------------------------------
428
+ // Briefing / Discourse Summary
429
+ // ---------------------------------------------------------------------------
430
+ export function formatBriefing(data) {
431
+ const lines = [];
432
+ const stats = data.statistics ?? {};
433
+ if (stats.total_episodes || stats.total_channels) {
434
+ lines.push(`**Briefing:** ${stats.total_episodes ?? "?"} episodes, ` +
435
+ `${stats.total_channels ?? "?"} channels, ` +
436
+ `${(stats.total_duration_hours ?? 0).toFixed(0)}h\n`);
437
+ }
438
+ for (let i = 0; i < (data.topics ?? []).length; i++) {
439
+ const t = data.topics[i];
440
+ const title = t.title ?? t.label ?? "?";
441
+ const segs = t.segment_count ?? 0;
442
+ const chs = t.channel_count ?? 0;
443
+ const score = t.score ?? 0;
444
+ lines.push(`${i + 1}. **${title}** (${segs} segments, ${chs} channels, score:${score.toFixed(2)})`);
445
+ const summary = t.summary ?? t.description;
446
+ if (summary)
447
+ lines.push(` ${truncate(summary, 250)}`);
448
+ for (const c of (t.citations ?? []).slice(0, 2)) {
449
+ const label = [c.speaker_name, c.channel_name].filter(Boolean).join(" on ");
450
+ if (c.text)
451
+ lines.push(` "${truncate(c.text, 120)}" — ${label}`);
452
+ }
453
+ lines.push("");
454
+ }
455
+ const events = data.events ?? [];
456
+ if (events.length) {
457
+ lines.push("**Key events:**");
458
+ for (const ev of events.slice(0, 5)) {
459
+ lines.push(`- ${ev.title ?? ev.description ?? "?"}`);
460
+ }
461
+ lines.push("");
462
+ }
463
+ const speakers = data.key_speakers ?? [];
464
+ if (speakers.length) {
465
+ const parts = speakers.slice(0, 8).map((s) => `${s.name ?? "?"} (${s.segment_count ?? "?"} seg)`);
466
+ lines.push(`**Key speakers:** ${parts.join(", ")}\n`);
467
+ }
468
+ if (data.synthesis)
469
+ lines.push(data.synthesis);
470
+ return lines.join("\n") || "No briefing data available.";
471
+ }
472
+ export function formatDiscourseSummary(data) {
473
+ const lines = [];
474
+ const synthesis = data.synthesis ??
475
+ data.summary ??
476
+ data.text ??
477
+ data.content;
478
+ if (synthesis)
479
+ lines.push(synthesis);
480
+ const topics = data.topics ?? [];
481
+ if (topics.length) {
482
+ lines.push("\n**Topics:**");
483
+ for (let i = 0; i < topics.length; i++) {
484
+ const t = topics[i];
485
+ const name = t.title ?? t.label ?? `Topic ${i + 1}`;
486
+ const count = t.segment_count;
487
+ const countStr = count ? ` (${count} segments)` : "";
488
+ lines.push(`${i + 1}. ${name}${countStr}`);
489
+ const desc = t.summary ?? t.description;
490
+ if (desc)
491
+ lines.push(` ${desc.slice(0, 200)}`);
492
+ }
493
+ }
494
+ const speakers = data.key_speakers ?? [];
495
+ if (speakers.length) {
496
+ lines.push("\n**Key Speakers:**");
497
+ for (const s of speakers.slice(0, 10)) {
498
+ lines.push(`- ${s.name ?? "?"}`);
499
+ }
500
+ }
501
+ return lines.join("\n") || "No discourse data available.";
502
+ }
503
+ // ---------------------------------------------------------------------------
504
+ // Topics
505
+ // ---------------------------------------------------------------------------
506
+ export function formatTopics(topics, totalClusters, partition) {
507
+ if (!topics.length)
508
+ return "No topic clusters found.";
509
+ const lines = [
510
+ `**${topics.length} of ${totalClusters ?? topics.length} topic clusters** (${partition ?? "?"})\n`,
511
+ ];
512
+ for (let i = 0; i < topics.length; i++) {
513
+ const t = topics[i];
514
+ const catTag = t.category ? ` [${t.category}]` : "";
515
+ lines.push(`${i + 1}. **${t.title ?? "?"}**${catTag}`);
516
+ lines.push(` ${t.segment_count ?? 0} segments across ${t.channel_count ?? 0} channels`);
517
+ const speakers = t.top_speakers ?? [];
518
+ if (speakers.length) {
519
+ lines.push(` Voices: ${speakers.slice(0, 3).map((s) => s.name ?? "?").join(", ")}`);
520
+ }
521
+ const topSeg = (t.top_segments ?? [])[0];
522
+ if (topSeg?.text) {
523
+ lines.push(` "${truncate(topSeg.text, 120)}" — ${topSeg.channel_name ?? ""}`);
524
+ }
525
+ lines.push("");
526
+ }
527
+ return lines.join("\n");
528
+ }
529
+ // ---------------------------------------------------------------------------
530
+ // Ad formatters
531
+ // ---------------------------------------------------------------------------
532
+ export function formatAdStats(data) {
533
+ const lines = ["# Ad Analytics Overview\n"];
534
+ lines.push(`**${(data.total_ad_hashes ?? 0).toLocaleString()}** confirmed ad sentences`);
535
+ lines.push(`**${data.total_brands ?? 0}** advertisers (+ ${(data.total_advertisers ?? 0) - (data.total_brands ?? 0)} network promos)`);
536
+ lines.push(`**${(data.ad_reads_in_window ?? 0).toLocaleString()}** ad reads in last ${data.time_window_days ?? 30}d`);
537
+ lines.push(`**${(data.episodes_with_ads_in_window ?? 0).toLocaleString()}** episodes with ads`);
538
+ lines.push(`**${data.avg_ad_read_duration_seconds ?? 0}s** avg ad read duration`);
539
+ const byCat = data.ads_by_category ?? data.by_category ?? {};
540
+ if (Object.keys(byCat).length) {
541
+ lines.push("\n**By Category:**");
542
+ const sorted = Object.entries(byCat).sort((a, b) => b[1] - a[1]);
543
+ for (const [cat, count] of sorted) {
544
+ lines.push(`- ${cat}: ${count.toLocaleString()}`);
545
+ }
546
+ }
547
+ const top = data.top_advertisers ?? [];
548
+ if (top.length) {
549
+ lines.push("\n**Top Advertisers:**");
550
+ for (let i = 0; i < top.length; i++) {
551
+ const a = top[i];
552
+ lines.push(`${i + 1}. **${a.name}** (${a.category ?? "?"}) — ` +
553
+ `${a.total_episode_count.toLocaleString()} episodes, ${a.total_channel_count} channels`);
554
+ }
555
+ }
556
+ return lines.join("\n");
557
+ }
558
+ export function formatAdvertiserList(data) {
559
+ const advs = data.advertisers ?? [];
560
+ if (!advs.length)
561
+ return "No advertisers found.";
562
+ const total = data.total ?? advs.length;
563
+ const lines = [`**${total} advertisers** (showing ${advs.length})\n`];
564
+ for (let i = 0; i < advs.length; i++) {
565
+ const a = advs[i];
566
+ const aliases = a.aliases?.length ? ` (aka ${a.aliases.slice(0, 3).join(", ")})` : "";
567
+ lines.push(`${i + 1}. **${a.name}**${aliases} [${a.category ?? "?"}] — ` +
568
+ `${a.total_episode_count.toLocaleString()} episodes, ${a.total_channel_count} channels`);
569
+ }
570
+ return lines.join("\n");
571
+ }
572
+ export function formatAdvertiserDetail(data) {
573
+ if (data.error)
574
+ return `Error: ${data.error}`;
575
+ const lines = [`# ${data.name}`];
576
+ if (data.category)
577
+ lines.push(`**Category:** ${data.category}`);
578
+ if (data.aliases?.length)
579
+ lines.push(`**Also known as:** ${data.aliases.join(", ")}`);
580
+ if (data.website)
581
+ lines.push(`**Website:** ${data.website}`);
582
+ lines.push(`**Episodes:** ${data.total_episode_count.toLocaleString()} | **Channels:** ${data.total_channel_count}`);
583
+ if (data.first_seen)
584
+ lines.push(`**Active:** ${fmtDate(data.first_seen)} — ${fmtDate(data.last_seen)}`);
585
+ const channels = data.top_channels ?? [];
586
+ if (channels.length) {
587
+ lines.push("\n**Top Channels:**");
588
+ for (const ch of channels.slice(0, 10)) {
589
+ lines.push(`- ${ch.name} (${ch.platform ?? "?"}) — ${ch.episode_count} episodes`);
590
+ }
591
+ }
592
+ const samples = data.sample_reads ?? [];
593
+ if (samples.length) {
594
+ lines.push("\n**Recent Ad Reads:**");
595
+ for (const s of samples) {
596
+ const dur = s.duration_seconds ? ` (${s.duration_seconds}s)` : "";
597
+ lines.push(`- [${fmtDate(s.date)}] ${s.episode_title ?? "?"}${dur}`);
598
+ if (s.text)
599
+ lines.push(` > ${s.text.slice(0, 200)}`);
600
+ }
601
+ }
602
+ return lines.join("\n");
603
+ }
604
+ export function formatChannelAds(data) {
605
+ if (data.error)
606
+ return `Error: ${data.error}`;
607
+ const chName = data.channel?.name ?? data.channel_name ?? "Channel";
608
+ const lines = [`# Ads on ${chName}`];
609
+ lines.push(`**${(data.total_ad_reads ?? 0).toLocaleString()}** ad reads in last ${data.time_window_days ?? 30}d\n`);
610
+ for (let i = 0; i < data.advertisers.length; i++) {
611
+ const a = data.advertisers[i];
612
+ const promo = a.is_network_promo ? " [network]" : "";
613
+ lines.push(`${i + 1}. **${a.name}**${promo} [${a.category ?? "?"}] — ` +
614
+ `${a.episode_count} episodes, ${a.read_count} reads`);
615
+ }
616
+ return lines.join("\n") || "No ad data for this channel.";
617
+ }
618
+ export function formatAdTrends(data) {
619
+ if (!data.advertisers?.length)
620
+ return "No trending data available.";
621
+ const lines = [`# Ad Trends (${data.comparison ?? ""})\n`];
622
+ for (const a of data.advertisers) {
623
+ const icon = { growing: "+", declining: "-", new: "NEW", gone: "GONE", stable: "=" }[a.trend] ?? "?";
624
+ lines.push(`[${icon}] **${a.name}** (${a.category ?? "?"}) — ` +
625
+ `${a.current_episodes} eps (was ${a.prior_episodes}, ${a.change_pct > 0 ? "+" : ""}${a.change_pct.toFixed(0)}%)`);
626
+ }
627
+ return lines.join("\n");
628
+ }
629
+ // ---------------------------------------------------------------------------
630
+ // Claims
631
+ // ---------------------------------------------------------------------------
632
+ export function formatClaims(data) {
633
+ const claims = data.claims ?? [];
634
+ if (!claims.length)
635
+ return "No claims found matching your search criteria.";
636
+ const total = data.total_results ?? claims.length;
637
+ const ms = data.processing_time_ms ?? 0;
638
+ const mode = data.search_mode ?? "";
639
+ const lines = [`**${total} claims** (showing ${claims.length}, ${ms.toFixed(0)}ms, mode: ${mode})\n`];
640
+ for (let i = 0; i < claims.length; i++) {
641
+ const c = claims[i];
642
+ const obs = c.observation_count ?? 0;
643
+ const eps = c.episode_count ?? 0;
644
+ const chs = c.channel_count ?? 0;
645
+ const sim = c.similarity;
646
+ const topic = c.topic ?? "";
647
+ const stance = c.stance ?? "";
648
+ const lastSeen = (c.last_seen ?? "").slice(0, 10);
649
+ const simStr = sim != null ? ` sim:${sim.toFixed(2)}` : "";
650
+ const tag = topic ? ` [${topic}]` : "";
651
+ const stanceStr = stance ? ` (${stance})` : "";
652
+ lines.push(`${i + 1}. ${c.summary}${tag}${stanceStr}${simStr}`);
653
+ lines.push(` ${obs} observations, ${eps} episodes, ${chs} channels, last: ${lastSeen}`);
654
+ const entities = c.entities ?? [];
655
+ if (entities.length)
656
+ lines.push(` Entities: ${entities.slice(0, 6).join(", ")}`);
657
+ const speakers = c.top_speakers ?? [];
658
+ if (speakers.length) {
659
+ const parts = speakers.slice(0, 3).map((s) => `${s.name} (${s.observation_count})`);
660
+ lines.push(` Speakers: ${parts.join(", ")}`);
661
+ }
662
+ const channels = c.top_channels ?? [];
663
+ if (channels.length) {
664
+ const parts = channels.slice(0, 3).map((ch) => `${ch.name} (${ch.observation_count})`);
665
+ lines.push(` Channels: ${parts.join(", ")}`);
666
+ }
667
+ const samples = c.sample_observations ?? [];
668
+ for (const s of samples.slice(0, 2)) {
669
+ const date = (s.publish_date ?? "").slice(0, 10);
670
+ const ep = truncate(s.episode_title, 60);
671
+ const ch = s.channel_name ?? "?";
672
+ const spk = s.speaker_name ? ` (${s.speaker_name})` : "";
673
+ lines.push(` - [${date}] ${ch} — ${ep}${spk}`);
674
+ }
675
+ lines.push("");
676
+ }
677
+ if (data.has_more) {
678
+ lines.push(`_${total - claims.length} more claims available (use offset parameter)_`);
679
+ }
680
+ return lines.join("\n");
681
+ }
682
+ //# sourceMappingURL=format.js.map