@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/cli.js ADDED
@@ -0,0 +1,737 @@
1
+ /** Signal4 CLI — terminal interface sharing handlers with the MCP server. */
2
+ import { applyConfig } from "./config.js";
3
+ import { HANDLERS } from "./handlers/registry.js";
4
+ import * as client from "./client.js";
5
+ import { writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { homedir } from "node:os";
8
+ import { createInterface } from "node:readline/promises";
9
+ function parseArgs(argv, specs) {
10
+ const flags = {};
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 : "");
15
+ // Build lookup: --name and -alias -> spec
16
+ const lookup = new Map();
17
+ for (const s of specs) {
18
+ lookup.set(`--${s.name}`, s);
19
+ if (s.alias)
20
+ lookup.set(`-${s.alias}`, s);
21
+ }
22
+ let i = 0;
23
+ while (i < argv.length) {
24
+ const arg = argv[i];
25
+ const spec = lookup.get(arg);
26
+ if (spec) {
27
+ if (spec.type === "boolean") {
28
+ flags[spec.name] = true;
29
+ }
30
+ else {
31
+ const val = argv[++i];
32
+ flags[spec.name] = spec.type === "number" ? Number(val) : (val ?? "");
33
+ }
34
+ }
35
+ else if (arg.startsWith("-")) {
36
+ // Unknown flag — skip
37
+ }
38
+ else {
39
+ positionals.push(arg);
40
+ }
41
+ i++;
42
+ }
43
+ return { positionals, flags };
44
+ }
45
+ // ---------------------------------------------------------------------------
46
+ // Shared flag specs
47
+ // ---------------------------------------------------------------------------
48
+ const FLAG_DAYS = { name: "days", alias: "d", type: "number", default: 30 };
49
+ const FLAG_LIMIT = { name: "limit", alias: "n", type: "number", default: 20 };
50
+ const FLAG_OFFSET = { name: "offset", alias: "o", type: "number", default: 0 };
51
+ const FLAG_SPEAKER = { name: "speaker", alias: "s", type: "string" };
52
+ const FLAG_CHANNEL = { name: "channel", alias: "c", type: "string" };
53
+ const FLAG_LANGUAGE = { name: "language", alias: "l", type: "string" };
54
+ const FLAG_MODE = { name: "mode", alias: "m", type: "string", default: "semantic" };
55
+ const FLAG_SKIP_CACHE = { name: "skip-cache", type: "boolean" };
56
+ const FLAG_JSON = { name: "json", type: "boolean" };
57
+ function daysRange(days) {
58
+ const end = new Date().toISOString().slice(0, 10);
59
+ const start = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
60
+ return { start_date: start, end_date: end };
61
+ }
62
+ function print(text) {
63
+ process.stdout.write(text + "\n");
64
+ }
65
+ function printJson(data) {
66
+ print(JSON.stringify(data, null, 2));
67
+ }
68
+ async function runHandler(tool, params) {
69
+ const handler = HANDLERS[tool];
70
+ if (!handler) {
71
+ print(`Error: handler "${tool}" not found.`);
72
+ process.exit(1);
73
+ }
74
+ const result = await handler(params);
75
+ if (result.isError) {
76
+ process.stderr.write((result.content[0]?.text ?? "Unknown error") + "\n");
77
+ process.exit(1);
78
+ }
79
+ print(result.content[0]?.text ?? "");
80
+ }
81
+ const COMMANDS = {
82
+ search: {
83
+ tool: "search_quotes",
84
+ description: "Search quotes by topic and/or speaker",
85
+ specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_DAYS, FLAG_MODE, FLAG_LIMIT, FLAG_OFFSET, FLAG_LANGUAGE,
86
+ { name: "sort", type: "string", default: "relevance" },
87
+ { name: "count", type: "boolean" },
88
+ { name: "speaker-id", type: "number" },
89
+ { name: "start", type: "string" }, { name: "end", type: "string" },
90
+ FLAG_JSON],
91
+ run: async (args, json) => {
92
+ const [topic] = args.positionals;
93
+ const f = args.flags;
94
+ const days = Number(f.days) || 30;
95
+ const dr = f.start ? { start_date: f.start, end_date: f.end || new Date().toISOString().slice(0, 10) } : daysRange(days);
96
+ const params = {
97
+ topic, speaker_name: f.speaker || undefined,
98
+ speaker_id: Number(f["speaker-id"]) || undefined,
99
+ channel_name: f.channel || undefined,
100
+ days, ...dr,
101
+ mode: f.mode, limit: Number(f.limit), offset: Number(f.offset),
102
+ count_only: f.count, sort: f.sort,
103
+ };
104
+ if (json)
105
+ return printJson(await client.searchQuotes({
106
+ topic: params.topic, speaker_name: params.speaker_name,
107
+ speaker_identity_id: params.speaker_id, channel_name: params.channel_name,
108
+ start_date: params.start_date, end_date: params.end_date,
109
+ mode: params.mode !== "semantic" ? params.mode : undefined,
110
+ limit: params.count_only ? 1 : Math.min(params.limit, 50), offset: params.offset,
111
+ }));
112
+ await runHandler("search_quotes", params);
113
+ },
114
+ },
115
+ claims: {
116
+ tool: "search_claims",
117
+ description: "Search extracted claims",
118
+ specs: [{ name: "entity", alias: "e", type: "string" }, FLAG_SPEAKER, FLAG_CHANNEL,
119
+ { name: "category", type: "string" }, { name: "stance", type: "string" },
120
+ FLAG_LANGUAGE, FLAG_MODE,
121
+ { name: "sort", type: "string", default: "relevance" },
122
+ { name: "min-obs", type: "number", default: 1 },
123
+ FLAG_DAYS, FLAG_LIMIT, FLAG_OFFSET, FLAG_JSON],
124
+ run: async (args, json) => {
125
+ const [topic] = args.positionals;
126
+ const f = args.flags;
127
+ const days = Number(f.days) || 30;
128
+ const params = {
129
+ topic, entity: f.entity || undefined,
130
+ speaker_name: f.speaker || undefined,
131
+ channel_name: f.channel || undefined,
132
+ category: f.category || undefined,
133
+ stance: f.stance || undefined,
134
+ language: f.language || undefined,
135
+ days, mode: f.mode,
136
+ sort: f.sort, min_observations: Number(f["min-obs"]) || 1,
137
+ limit: Number(f.limit), offset: Number(f.offset),
138
+ };
139
+ if (json)
140
+ return printJson(await client.searchClaims({
141
+ topic: params.topic, entity: params.entity, speaker_name: params.speaker_name,
142
+ channel_names: params.channel_name ? [params.channel_name] : undefined,
143
+ primary_topic: params.category, stance: params.stance, language: params.language,
144
+ days: params.days, mode: params.mode, sort: params.sort,
145
+ min_observations: params.min_observations > 1 ? params.min_observations : undefined,
146
+ limit: Math.min(params.limit, 50), offset: params.offset,
147
+ }));
148
+ await runHandler("search_claims", params);
149
+ },
150
+ },
151
+ trend: {
152
+ tool: "detect_trend",
153
+ description: "Detect topic volume trends",
154
+ specs: [{ name: "recent", type: "number", default: 7 },
155
+ { name: "baseline", type: "number", default: 30 },
156
+ { name: "min-similarity", type: "number" }, FLAG_JSON],
157
+ run: async (args, json) => {
158
+ const [topic] = args.positionals;
159
+ if (!topic) {
160
+ print("Error: topic is required.");
161
+ process.exit(1);
162
+ }
163
+ const f = args.flags;
164
+ const params = {
165
+ topic, recent_days: Number(f.recent) || 7, baseline_days: Number(f.baseline) || 30,
166
+ min_similarity: Number(f["min-similarity"]) || undefined,
167
+ };
168
+ if (json)
169
+ return printJson(await client.detectTrend(params));
170
+ await runHandler("detect_trend", params);
171
+ },
172
+ },
173
+ speaker: {
174
+ tool: "find_speakers",
175
+ description: "Speaker search, profile, or discourse",
176
+ specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_OFFSET,
177
+ { name: "segments", type: "boolean" }, FLAG_JSON],
178
+ run: async (args, json) => {
179
+ const [identity, topic] = args.positionals;
180
+ if (!identity) {
181
+ print("Error: speaker name or ID required.");
182
+ process.exit(1);
183
+ }
184
+ const f = args.flags;
185
+ const isId = /^\d+$/.test(identity);
186
+ const days = Number(f.days) || 30;
187
+ const limit = Number(f.limit) || 20;
188
+ // Profile by ID
189
+ if (isId) {
190
+ const sid = Number(identity);
191
+ if (f.segments) {
192
+ const params = { speaker_id: sid, sort: "chronological", days, limit, offset: Number(f.offset) };
193
+ if (json)
194
+ return printJson(await client.getSpeakerSegments({ identity_id: sid, days, limit, offset: Number(f.offset) }));
195
+ await runHandler("search_quotes", params);
196
+ }
197
+ else if (topic) {
198
+ const params = { speaker_id: sid, topic, days, limit };
199
+ if (json)
200
+ return printJson(await client.searchSpeakerSegments({ identity_id: sid, query: topic, days, limit }));
201
+ await runHandler("search_quotes", params);
202
+ }
203
+ else {
204
+ if (json)
205
+ return printJson(await client.getSpeaker(sid));
206
+ await runHandler("get_speaker", { speaker_id: sid });
207
+ }
208
+ return;
209
+ }
210
+ // Segments by name
211
+ if (f.segments) {
212
+ const params = { speaker_name: identity, sort: "chronological", days, limit, offset: Number(f.offset) };
213
+ if (json) {
214
+ const byName = await client.getSpeakerByName(identity);
215
+ const sid = byName.speaker_id ?? byName.id;
216
+ if (sid)
217
+ return printJson(await client.getSpeakerSegments({ identity_id: sid, days, limit, offset: Number(f.offset) }));
218
+ return printJson({ error: `Speaker "${identity}" not found` });
219
+ }
220
+ await runHandler("search_quotes", params);
221
+ return;
222
+ }
223
+ // Discourse search (name + topic)
224
+ if (topic) {
225
+ const params = { speaker_name: identity, topic, days, limit };
226
+ if (json)
227
+ return printJson(await client.searchQuotes({ topic, speaker_name: identity, limit }));
228
+ await runHandler("search_quotes", params);
229
+ return;
230
+ }
231
+ // Name search, auto-escalate on single match
232
+ if (json) {
233
+ const results = await client.searchSpeakers(identity, limit);
234
+ if (Array.isArray(results) && results.length === 1) {
235
+ const sid = results[0].identity_id ?? results[0].id;
236
+ if (sid)
237
+ return printJson(await client.getSpeaker(Number(sid)));
238
+ }
239
+ return printJson(results);
240
+ }
241
+ const result = await HANDLERS.find_speakers({ query: identity, limit });
242
+ print(result.content[0]?.text ?? "");
243
+ },
244
+ },
245
+ channel: {
246
+ tool: "get_channel",
247
+ description: "Channel profile",
248
+ specs: [FLAG_JSON],
249
+ run: async (args, json) => {
250
+ const [id] = args.positionals;
251
+ if (!id) {
252
+ print("Error: channel ID or name required.");
253
+ process.exit(1);
254
+ }
255
+ const isId = /^\d+$/.test(id);
256
+ if (json)
257
+ return printJson(isId ? await client.getChannel(Number(id)) : await client.getChannelByName(id));
258
+ await runHandler("get_channel", isId ? { channel_id: Number(id) } : { channel_name: id });
259
+ },
260
+ },
261
+ episode: {
262
+ tool: "get_episode",
263
+ description: "Episode details or transcript",
264
+ specs: [{ name: "transcript", type: "boolean" }, FLAG_JSON],
265
+ run: async (args, json) => {
266
+ const [id] = args.positionals;
267
+ if (!id) {
268
+ print("Error: content ID required.");
269
+ process.exit(1);
270
+ }
271
+ if (json) {
272
+ const data = await client.getEpisode(id);
273
+ if (args.flags.transcript)
274
+ data.transcript = await client.getTranscript(id);
275
+ return printJson(data);
276
+ }
277
+ await runHandler("get_episode", { content_id: id, include_transcript: args.flags.transcript });
278
+ },
279
+ },
280
+ segment: {
281
+ tool: "get_segment",
282
+ description: "Segment detail with audio clip URL",
283
+ specs: [FLAG_JSON],
284
+ run: async (args, json) => {
285
+ const [id] = args.positionals;
286
+ if (!id) {
287
+ print("Error: segment ID required.");
288
+ process.exit(1);
289
+ }
290
+ if (json)
291
+ return printJson(await client.getSegment(Number(id)));
292
+ await runHandler("get_segment", { segment_id: Number(id) });
293
+ },
294
+ },
295
+ thread: {
296
+ tool: "get_thread",
297
+ description: "Conversation context around a quote",
298
+ specs: [{ name: "n", type: "number", default: 5 }, FLAG_JSON],
299
+ run: async (args, json) => {
300
+ const [id] = args.positionals;
301
+ if (!id) {
302
+ print("Error: segment ID required.");
303
+ process.exit(1);
304
+ }
305
+ const n = Number(args.flags.n) || 5;
306
+ if (json)
307
+ return printJson(await client.getSegmentThread(Number(id), n));
308
+ await runHandler("get_thread", { segment_id: Number(id), n });
309
+ },
310
+ },
311
+ analyze: {
312
+ tool: "analyze_topic",
313
+ description: "AI-powered topic analysis",
314
+ specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_DAYS,
315
+ { name: "workflow", alias: "w", type: "string", default: "simple_rag" },
316
+ FLAG_SKIP_CACHE, FLAG_JSON],
317
+ run: async (args, json) => {
318
+ const [query] = args.positionals;
319
+ if (!query) {
320
+ print("Error: query is required.");
321
+ process.exit(1);
322
+ }
323
+ const f = args.flags;
324
+ process.stderr.write("Analyzing... (this may take up to 60 seconds)\n");
325
+ const params = {
326
+ query, workflow: f.workflow,
327
+ speaker_name: f.speaker || undefined,
328
+ channel: f.channel || undefined,
329
+ days: Number(f.days) || undefined,
330
+ skip_cache: f["skip-cache"],
331
+ };
332
+ if (json)
333
+ return printJson(await client.analyze(params));
334
+ await runHandler("analyze_topic", params);
335
+ },
336
+ },
337
+ compare: {
338
+ tool: "compare_speakers",
339
+ description: "Compare speakers on a topic",
340
+ specs: [{ name: "speakers", type: "string" }, FLAG_DAYS, FLAG_SKIP_CACHE, FLAG_JSON],
341
+ run: async (args, json) => {
342
+ const [topic] = args.positionals;
343
+ if (!topic) {
344
+ print("Error: topic is required.");
345
+ process.exit(1);
346
+ }
347
+ const f = args.flags;
348
+ const speakers = (f.speakers || "").split(",").map((s) => s.trim()).filter(Boolean);
349
+ if (speakers.length < 2) {
350
+ print("Error: --speakers requires 2-5 comma-separated names.");
351
+ process.exit(1);
352
+ }
353
+ process.stderr.write("Comparing speakers... (this may take up to 60 seconds)\n");
354
+ const params = {
355
+ topic, speakers, days: Number(f.days) || 90, skip_cache: f["skip-cache"],
356
+ };
357
+ if (json)
358
+ return printJson(await client.analyze({ query: topic, workflow: "speaker_compare", speaker_names: speakers, days: params.days, skip_cache: params.skip_cache }));
359
+ await runHandler("compare_speakers", params);
360
+ },
361
+ },
362
+ dossier: {
363
+ tool: "build_dossier",
364
+ description: "Comprehensive speaker briefing on a topic",
365
+ specs: [FLAG_SPEAKER, FLAG_DAYS, FLAG_SKIP_CACHE, FLAG_JSON],
366
+ run: async (args, json) => {
367
+ const [topic] = args.positionals;
368
+ if (!topic) {
369
+ print("Error: topic is required.");
370
+ process.exit(1);
371
+ }
372
+ const speaker = args.flags.speaker;
373
+ if (!speaker) {
374
+ print("Error: --speaker is required.");
375
+ process.exit(1);
376
+ }
377
+ process.stderr.write("Building dossier... (this may take up to 60 seconds)\n");
378
+ const params = {
379
+ topic, speaker_name: speaker, days: Number(args.flags.days) || 180, skip_cache: args.flags["skip-cache"],
380
+ };
381
+ if (json)
382
+ return printJson(await client.analyze({ query: topic, workflow: "speaker_dossier", speaker_name: speaker, days: params.days, skip_cache: params.skip_cache }));
383
+ await runHandler("build_dossier", params);
384
+ },
385
+ },
386
+ contradictions: {
387
+ tool: "check_contradictions",
388
+ description: "Find contradictory statements from a speaker",
389
+ specs: [FLAG_SPEAKER, FLAG_DAYS, FLAG_SKIP_CACHE, FLAG_JSON],
390
+ run: async (args, json) => {
391
+ const [topic] = args.positionals;
392
+ if (!topic) {
393
+ print("Error: topic is required.");
394
+ process.exit(1);
395
+ }
396
+ const speaker = args.flags.speaker;
397
+ if (!speaker) {
398
+ print("Error: --speaker is required.");
399
+ process.exit(1);
400
+ }
401
+ process.stderr.write("Checking contradictions...\n");
402
+ const params = {
403
+ topic, speaker_name: speaker, days: Number(args.flags.days) || 365, skip_cache: args.flags["skip-cache"],
404
+ };
405
+ if (json)
406
+ return printJson(await client.analyze({ query: topic, workflow: "contradiction_check", speaker_name: speaker, days: params.days, skip_cache: params.skip_cache }));
407
+ await runHandler("check_contradictions", params);
408
+ },
409
+ },
410
+ briefing: {
411
+ tool: "get_briefing",
412
+ description: "AI discourse briefing",
413
+ specs: [FLAG_DAYS, { name: "max-topics", type: "number", default: 8 }, FLAG_JSON],
414
+ run: async (args, json) => {
415
+ const f = args.flags;
416
+ const params = { days: Number(f.days) || 3, max_topics: Number(f["max-topics"]) || 8 };
417
+ if (json)
418
+ return printJson(await client.getDiscourseSummary(params));
419
+ await runHandler("get_briefing", params);
420
+ },
421
+ },
422
+ stats: {
423
+ tool: "get_platform_stats",
424
+ description: "Platform statistics",
425
+ specs: [FLAG_DAYS, { name: "projects", type: "boolean" }, FLAG_JSON],
426
+ run: async (args, json) => {
427
+ const f = args.flags;
428
+ if (f.projects) {
429
+ if (json)
430
+ return printJson(await client.getProjectStats());
431
+ await runHandler("get_project_stats", {});
432
+ }
433
+ else {
434
+ const days = Number(f.days) || 30;
435
+ if (json)
436
+ return printJson(await client.getReportStats(days));
437
+ await runHandler("get_platform_stats", { days });
438
+ }
439
+ },
440
+ },
441
+ "top-channels": {
442
+ tool: "get_top_channels",
443
+ description: "Top channels by episode count",
444
+ specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_LANGUAGE, FLAG_JSON],
445
+ run: async (args, json) => {
446
+ const f = args.flags;
447
+ const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 15, language: f.language || undefined };
448
+ if (json)
449
+ return printJson(await client.getTopChannels(params));
450
+ await runHandler("get_top_channels", params);
451
+ },
452
+ },
453
+ "top-speakers": {
454
+ tool: "get_top_speakers",
455
+ description: "Top speakers by appearances, talk time, or clips",
456
+ specs: [FLAG_DAYS, FLAG_LIMIT, { name: "ranking", alias: "r", type: "string", default: "appearances" }, FLAG_JSON],
457
+ run: async (args, json) => {
458
+ const f = args.flags;
459
+ const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 15, ranking: f.ranking };
460
+ if (json)
461
+ return printJson(await client.getTopSpeakers(params));
462
+ await runHandler("get_top_speakers", params);
463
+ },
464
+ },
465
+ network: {
466
+ tool: "get_speaker_network",
467
+ description: "Speaker co-appearance network",
468
+ specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_LANGUAGE, FLAG_JSON],
469
+ run: async (args, json) => {
470
+ const f = args.flags;
471
+ const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 20, language: f.language || "en" };
472
+ if (json)
473
+ return printJson(await client.getSpeakerNetwork(params));
474
+ await runHandler("get_speaker_network", params);
475
+ },
476
+ },
477
+ topics: {
478
+ tool: "get_topics",
479
+ description: "Trending topic clusters",
480
+ specs: [FLAG_LANGUAGE, FLAG_LIMIT, { name: "category", alias: "c", type: "string" },
481
+ { name: "min-segments", type: "number", default: 50 }, FLAG_JSON],
482
+ run: async (args, json) => {
483
+ const f = args.flags;
484
+ const params = {
485
+ language: f.language || "en", limit: Number(f.limit) || 20,
486
+ category: f.category || undefined, min_segments: Number(f["min-segments"]) || 50,
487
+ };
488
+ if (json)
489
+ return printJson(await client.getTopics(params));
490
+ await runHandler("get_topics", params);
491
+ },
492
+ },
493
+ ads: {
494
+ tool: "get_ad_analytics",
495
+ description: "Ad intelligence",
496
+ specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_OFFSET,
497
+ { name: "advertisers", type: "boolean" }, { name: "trending", type: "boolean" },
498
+ { name: "channel", alias: "c", type: "number" },
499
+ { name: "category", type: "string" }, { name: "sort", type: "string", default: "episodes" },
500
+ FLAG_JSON],
501
+ run: async (args, json) => {
502
+ const [adId] = args.positionals;
503
+ const f = args.flags;
504
+ const days = Number(f.days) || 30;
505
+ if (adId && /^\d+$/.test(adId)) {
506
+ if (json)
507
+ return printJson(await client.getAdvertiser(Number(adId)));
508
+ await runHandler("get_ad_analytics", { view: "advertiser", advertiser_id: Number(adId), days });
509
+ }
510
+ else if (f.advertisers) {
511
+ if (json)
512
+ return printJson(await client.getAdvertisers({ category: f.category || undefined, sort_by: f.sort, limit: Number(f.limit), offset: Number(f.offset) }));
513
+ await runHandler("get_ad_analytics", { view: "advertisers", category: f.category || undefined, days, limit: Number(f.limit) });
514
+ }
515
+ else if (f.trending) {
516
+ if (json)
517
+ return printJson(await client.getAdTrends(days));
518
+ await runHandler("get_ad_analytics", { view: "trending", days });
519
+ }
520
+ else if (Number(f.channel)) {
521
+ if (json)
522
+ return printJson(await client.getAdsByChannel(Number(f.channel), days));
523
+ await runHandler("get_ad_analytics", { view: "channel", channel_id: Number(f.channel), days });
524
+ }
525
+ else {
526
+ if (json)
527
+ return printJson(await client.getAdStats(days));
528
+ await runHandler("get_ad_analytics", { view: "overview", days });
529
+ }
530
+ },
531
+ },
532
+ bookmarks: {
533
+ tool: "manage_bookmarks",
534
+ description: "Manage bookmarks",
535
+ specs: [{ name: "type", type: "string" }, { name: "search", type: "string" },
536
+ FLAG_LIMIT, FLAG_OFFSET, { name: "note", type: "string" }, FLAG_JSON],
537
+ run: async (args, json) => {
538
+ const [sub, entityType, entityId] = args.positionals;
539
+ const f = args.flags;
540
+ if (sub === "add" && entityType && entityId) {
541
+ const params = { action: "create", entity_type: entityType, entity_id: entityId, note: f.note || undefined };
542
+ if (json)
543
+ return printJson(await client.createBookmark({ entity_type: entityType, entity_id: entityId, note: f.note || undefined }));
544
+ await runHandler("manage_bookmarks", params);
545
+ }
546
+ else if (sub === "rm" && entityType) {
547
+ const bookmarkId = Number(entityType);
548
+ if (json)
549
+ return printJson(await client.deleteBookmark(bookmarkId));
550
+ await runHandler("manage_bookmarks", { action: "delete", bookmark_id: bookmarkId });
551
+ }
552
+ else {
553
+ const params = {
554
+ action: "list", entity_type: f.type || undefined,
555
+ search: f.search || undefined,
556
+ limit: Number(f.limit) || 20, offset: Number(f.offset),
557
+ };
558
+ if (json)
559
+ return printJson(await client.listBookmarks({
560
+ entity_type: f.type || undefined,
561
+ search: f.search || undefined,
562
+ limit: Number(f.limit), offset: Number(f.offset),
563
+ }));
564
+ await runHandler("manage_bookmarks", params);
565
+ }
566
+ },
567
+ },
568
+ };
569
+ // ---------------------------------------------------------------------------
570
+ // Setup command
571
+ // ---------------------------------------------------------------------------
572
+ async function cmdSetup(force) {
573
+ const configDir = join(homedir(), ".signal4");
574
+ const configPath = join(configDir, "config");
575
+ if (existsSync(configPath) && !force) {
576
+ print("Config already exists at ~/.signal4/config. Use --force to overwrite.");
577
+ process.exit(1);
578
+ }
579
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
580
+ const key = await rl.question("API key: ");
581
+ rl.close();
582
+ if (!key.trim()) {
583
+ print("Error: no API key provided.");
584
+ process.exit(1);
585
+ }
586
+ process.env.SIGNAL4_API_KEY = key.trim();
587
+ try {
588
+ const who = await client.whoami();
589
+ print(`Authenticated as ${who.user_email ?? who.key_prefix ?? "unknown"}`);
590
+ }
591
+ catch (err) {
592
+ print(`Error: invalid API key (${err instanceof Error ? err.message : String(err)})`);
593
+ process.exit(1);
594
+ }
595
+ if (!existsSync(configDir))
596
+ mkdirSync(configDir, { recursive: true });
597
+ writeFileSync(configPath, `api_key=${key.trim()}\n`, { mode: 0o600 });
598
+ try {
599
+ chmodSync(configPath, 0o600);
600
+ }
601
+ catch { /* Windows */ }
602
+ print(`Saved to ${configPath}`);
603
+ }
604
+ // ---------------------------------------------------------------------------
605
+ // Whoami command
606
+ // ---------------------------------------------------------------------------
607
+ async function cmdWhoami(json) {
608
+ const data = await client.whoami();
609
+ if (json)
610
+ return printJson(data);
611
+ const lines = [];
612
+ if (data.key_prefix)
613
+ lines.push(`Key: ${data.key_prefix}...`);
614
+ if (data.user_email)
615
+ lines.push(`Email: ${data.user_email}`);
616
+ if (data.allowed_projects?.length)
617
+ lines.push(`Projects: ${data.allowed_projects.join(", ")}`);
618
+ if (data.scopes?.length)
619
+ lines.push(`Scopes: ${data.scopes.join(", ")}`);
620
+ if (data.full_access)
621
+ lines.push("Access: full");
622
+ if (data.available_commands?.length)
623
+ lines.push(`Commands: ${data.available_commands.join(", ")}`);
624
+ print(lines.join("\n"));
625
+ }
626
+ // ---------------------------------------------------------------------------
627
+ // Help
628
+ // ---------------------------------------------------------------------------
629
+ function showHelp() {
630
+ const lines = [
631
+ "Signal4 — media intelligence CLI & MCP server",
632
+ "",
633
+ "Usage: s4 <command> [args] [flags]",
634
+ " s4 --mcp Start MCP server (stdio transport)",
635
+ "",
636
+ "Search:",
637
+ ' s4 search "immigration" --speaker "Trudeau" --days 7',
638
+ ' s4 claims "AI regulation" --entity "OpenAI"',
639
+ ' s4 trend "tariffs" --recent 7 --baseline 30',
640
+ "",
641
+ "Explore:",
642
+ ' s4 speaker "Tucker Carlson"',
643
+ ' s4 speaker "Tucker Carlson" "economy" # discourse',
644
+ ' s4 channel "Power & Politics"',
645
+ " s4 episode pod_8f1e55513f70 --transcript",
646
+ " s4 segment 28553756",
647
+ " s4 thread 28553756 --n 10",
648
+ "",
649
+ "Analyze:",
650
+ ' s4 analyze "carbon tax" --days 14',
651
+ ' s4 compare "tariffs" --speakers "Trump,Biden"',
652
+ ' s4 dossier "immigration" --speaker "Carlson"',
653
+ ' s4 contradictions "vaccines" --speaker "Trump"',
654
+ " s4 briefing --days 7",
655
+ "",
656
+ "Reports:",
657
+ " s4 stats --days 30",
658
+ " s4 top-speakers --ranking talkative",
659
+ " s4 top-channels --limit 25",
660
+ " s4 network",
661
+ " s4 topics --category economy",
662
+ " s4 ads --trending",
663
+ "",
664
+ "Data:",
665
+ " s4 bookmarks",
666
+ " s4 bookmarks add segment 12345 --note 'key quote'",
667
+ " s4 bookmarks rm 7",
668
+ "",
669
+ "Account:",
670
+ " s4 setup Save API key",
671
+ " s4 whoami Show key info",
672
+ "",
673
+ "Flags:",
674
+ " --json Raw JSON output",
675
+ " --help, -h Show this help",
676
+ "",
677
+ "MCP server:",
678
+ " s4 mcp Start MCP server for Claude Desktop / Cursor",
679
+ ];
680
+ print(lines.join("\n"));
681
+ }
682
+ // ---------------------------------------------------------------------------
683
+ // Main
684
+ // ---------------------------------------------------------------------------
685
+ export async function main() {
686
+ applyConfig();
687
+ const argv = process.argv.slice(2);
688
+ // Help
689
+ if (!argv.length || argv.includes("--help") || argv.includes("-h")) {
690
+ showHelp();
691
+ process.exit(0);
692
+ }
693
+ // MCP mode
694
+ if (argv[0] === "mcp" || argv.includes("--mcp")) {
695
+ await import("./index.js");
696
+ return;
697
+ }
698
+ const command = argv[0];
699
+ const rest = argv.slice(1);
700
+ // Setup (no API key needed)
701
+ if (command === "setup") {
702
+ await cmdSetup(rest.includes("--force"));
703
+ return;
704
+ }
705
+ // Check API key
706
+ if (!process.env.SIGNAL4_API_KEY) {
707
+ process.stderr.write("No API key configured. Run `s4 setup` or set SIGNAL4_API_KEY.\n");
708
+ process.exit(1);
709
+ }
710
+ // Whoami
711
+ if (command === "whoami") {
712
+ await cmdWhoami(rest.includes("--json"));
713
+ return;
714
+ }
715
+ // Lookup command
716
+ const cmd = COMMANDS[command];
717
+ if (!cmd) {
718
+ process.stderr.write(`Unknown command: ${command}\n\nRun s4 --help for usage.\n`);
719
+ process.exit(1);
720
+ }
721
+ // Parse args
722
+ const args = parseArgs(rest, cmd.specs);
723
+ const json = args.flags.json;
724
+ try {
725
+ await cmd.run(args, json);
726
+ }
727
+ catch (err) {
728
+ const msg = err instanceof Error ? err.message : String(err);
729
+ process.stderr.write(`Error: ${msg}\n`);
730
+ process.exit(1);
731
+ }
732
+ }
733
+ main().catch((err) => {
734
+ process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
735
+ process.exit(1);
736
+ });
737
+ //# sourceMappingURL=cli.js.map