@signal4/s4 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +102 -32
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +26 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +72 -1
- package/dist/client.js.map +1 -1
- package/dist/format.d.ts +28 -3
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +103 -21
- package/dist/format.js.map +1 -1
- package/dist/handlers/claim_detail.d.ts.map +1 -1
- package/dist/handlers/claim_detail.js +16 -8
- package/dist/handlers/claim_detail.js.map +1 -1
- package/dist/handlers/claims.d.ts.map +1 -1
- package/dist/handlers/claims.js +15 -6
- package/dist/handlers/claims.js.map +1 -1
- package/dist/handlers/count.d.ts +7 -0
- package/dist/handlers/count.d.ts.map +1 -1
- package/dist/handlers/count.js +13 -1
- package/dist/handlers/count.js.map +1 -1
- package/dist/handlers/explore.d.ts.map +1 -1
- package/dist/handlers/explore.js +3 -0
- package/dist/handlers/explore.js.map +1 -1
- package/dist/handlers/reports.d.ts +5 -0
- package/dist/handlers/reports.d.ts.map +1 -1
- package/dist/handlers/reports.js +101 -16
- package/dist/handlers/reports.js.map +1 -1
- package/dist/handlers/search.d.ts.map +1 -1
- package/dist/handlers/search.js +13 -2
- package/dist/handlers/search.js.map +1 -1
- package/dist/speed.d.ts.map +1 -1
- package/dist/speed.js +22 -4
- package/dist/speed.js.map +1 -1
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +1 -0
- package/dist/surface.js.map +1 -1
- package/dist/types.d.ts +98 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,11 @@ s4 count tariffs --since 2025 --by month # segments · episodes · channels
|
|
|
38
38
|
s4 count "carbon tax" -m keyword --collection Canadian --by channel # exact; rank shows by what they said
|
|
39
39
|
s4 count "carbon tax" --vs "carbon pricing" --since 2024 --by quarter # several topics side by side
|
|
40
40
|
s4 count "Strong Borders Act" -m keyword --by episode --order date -n 100 # every matching episode, newest first
|
|
41
|
+
s4 count "carbon tax" -m keyword --since 2025 --by speaker # who speaks in the matching segments, names corroborated per episode
|
|
42
|
+
s4 top-speakers --topic tariffs -d 90 # same ranking, from the transcripts (not episode titles)
|
|
43
|
+
s4 entity-claims "Donald Trump" # every entity row of that name + merges; says what it matched
|
|
44
|
+
s4 search tariffs --keyword --since 2025 --page 3 # pages of -n; --all follows every page
|
|
45
|
+
s4 top-channels --topic Medicaid --country us -d 365 # channels ranked by what they said
|
|
41
46
|
s4 segment 42314442 # full text of a quote (seg:ID from search)
|
|
42
47
|
s4 --version
|
|
43
48
|
```
|
|
@@ -45,9 +50,31 @@ s4 --version
|
|
|
45
50
|
Flags: `-n/-k/--limit` results · `--offset` skip · `-s` speaker · `-c` channel · `-d/--days` · `-m` mode ·
|
|
46
51
|
`-o/--out` file · `--since/--until` (or `--start/--end`; `2023`, `2023-06`, `2023-06-15`; on search, claims, timeline, trend, the claims-analysis commands, top-claims, top-channels, top-speakers) · `--collection` (every corpus command) · `--json`.
|
|
47
52
|
`--days` has no ceiling on search, claims, timeline and count; report commands cap at 365.
|
|
48
|
-
`count` groups with `--by` (day · week · month · quarter · year · channel · episode · language · country · platform · collection · lean);
|
|
53
|
+
`count` groups with `--by` (day · week · month · quarter · year · channel · episode · speaker · language · country · platform · collection · lean);
|
|
49
54
|
keyword counts are exact, semantic counts are estimates at similarity ≥0.35, and every language is counted unless `--language` names some. Errors go to stderr with exit 1.
|
|
50
55
|
|
|
56
|
+
## Paging
|
|
57
|
+
|
|
58
|
+
Every list command and tool pages the same way: `limit` + `offset` (CLI `-n` and `--offset`,
|
|
59
|
+
or `--page N`), and every answer ends with the next call (`next: offset:40`, CLI `--offset 40`).
|
|
60
|
+
`s4 <command> --all` follows the pages for you (up to 50). How deep each list goes:
|
|
61
|
+
|
|
62
|
+
| List | Depth |
|
|
63
|
+
|---|---|
|
|
64
|
+
| keyword search, `count` breakdowns, entity claims, claim observations | every match |
|
|
65
|
+
| semantic / hybrid quote search | rank 2,000 (`depth_reached` says when you hit it) |
|
|
66
|
+
| semantic / hybrid claims search | rank 1,000 |
|
|
67
|
+
| top speakers / channels / talk time | 200 (topic rankings: every match) |
|
|
68
|
+
| top claims | 100 |
|
|
69
|
+
|
|
70
|
+
Past a semantic depth, `--keyword` search or `count ... --by episode` list every match.
|
|
71
|
+
From TypeScript, `paginate()` walks the pages of any list call:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { paginate, searchQuotes } from '@signal4/s4/client';
|
|
75
|
+
for await (const quote of paginate(searchQuotes, { topic: 'tariffs', mode: 'keyword', days: 365 })) { /* ... */ }
|
|
76
|
+
```
|
|
77
|
+
|
|
51
78
|
## Speed
|
|
52
79
|
|
|
53
80
|
Every MCP tool description and every `s4 <command> --help` starts with how long the call takes at
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,6EAA6E;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,6EAA6E;AA0qD7E,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA+G1C"}
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { descriptionTag, estimate, startNotice, REPORT_ELAPSED_OVER_S } from "./
|
|
|
5
5
|
import * as client from "./client.js";
|
|
6
6
|
import { writeFileSync, mkdirSync, existsSync, chmodSync, readFileSync, statSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import { formatDossier } from "./format.js";
|
|
8
|
+
import { formatDossier, parsePoliticalLean } from "./format.js";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { createInterface } from "node:readline/promises";
|
|
11
11
|
import { parseArgs as parseArgsRaw, UsageError, didYouMean } from "./cli_args.js";
|
|
@@ -29,6 +29,12 @@ function parseArgs(argv, specs) {
|
|
|
29
29
|
const FLAG_DAYS = { name: "days", alias: "d", type: "number", default: 30, desc: "Time window in days (report commands cap at 365; search/claims/timeline do not)" };
|
|
30
30
|
const FLAG_LIMIT = { name: "limit", alias: ["n", "k"], type: "number", default: 20, desc: "Number of results (-n, -k)" };
|
|
31
31
|
const FLAG_OFFSET = { name: "offset", type: "number", default: 0, desc: "Skip this many results (pagination)" };
|
|
32
|
+
const FLAG_PAGE = { name: "page", type: "number", desc: "Page number, 1-based, in pages of --limit (sets --offset)" };
|
|
33
|
+
const FLAG_ALL = { name: "all", type: "boolean", desc: `Follow every page (up to ${50} pages)` };
|
|
34
|
+
/** Paging flags for every list command (signal4#329). */
|
|
35
|
+
const PAGING = [FLAG_OFFSET, FLAG_PAGE, FLAG_ALL];
|
|
36
|
+
/** --all stops after this many pages; the last page then says how to continue. */
|
|
37
|
+
const MAX_ALL_PAGES = 50;
|
|
32
38
|
const FLAG_SPEAKER = { name: "speaker", alias: "s", type: "string", desc: "Filter by speaker name (full name)" };
|
|
33
39
|
const FLAG_CHANNEL = { name: "channel", alias: "c", type: "string", desc: "Filter by channel name (comma-separated for multiple)" };
|
|
34
40
|
const FLAG_LANGUAGE = { name: "language", type: "string", desc: "Filter by language code (en, fr, ...)" };
|
|
@@ -42,7 +48,7 @@ const FLAG_SKIP_CACHE = { name: "skip-cache", type: "boolean", desc: "Force fres
|
|
|
42
48
|
const FLAG_OUT = { name: "out", alias: "o", long: ["output"], type: "string", desc: "Write the rendered output to a file (.md, or .csv on search)" };
|
|
43
49
|
const FLAG_JSON = { name: "json", type: "boolean", desc: "Raw JSON output" };
|
|
44
50
|
const GRANULARITIES = ["day", "week", "month", "quarter", "year"];
|
|
45
|
-
const COUNT_GROUPS = ["none", ...GRANULARITIES, "channel", "episode", "language", "country", "platform", "collection", "lean"];
|
|
51
|
+
const COUNT_GROUPS = ["none", ...GRANULARITIES, "channel", "episode", "speaker", "language", "country", "platform", "collection", "lean"];
|
|
46
52
|
/** --start/--end as tool params (already normalized to YYYY-MM-DD by parseArgs); absent when not given. */
|
|
47
53
|
function dateFlags(f) {
|
|
48
54
|
const out = {};
|
|
@@ -217,12 +223,40 @@ async function runHandler(tool, params, out) {
|
|
|
217
223
|
process.stderr.write((result.content[0]?.text ?? "Unknown error") + "\n");
|
|
218
224
|
process.exit(1);
|
|
219
225
|
}
|
|
220
|
-
|
|
226
|
+
let text = result.content[0]?.text ?? "";
|
|
227
|
+
// --all: follow the next-page hint every list answer ends with, until it stops.
|
|
228
|
+
let pages = 1;
|
|
229
|
+
let next = nextPage(text);
|
|
230
|
+
while (followAllPages && next && pages < MAX_ALL_PAGES) {
|
|
231
|
+
const current = Number(params[next.param] ?? 0);
|
|
232
|
+
if (next.offset <= current)
|
|
233
|
+
break;
|
|
234
|
+
params = { ...params, [next.param]: next.offset };
|
|
235
|
+
const more = await handler(params);
|
|
236
|
+
if (more.isError)
|
|
237
|
+
break;
|
|
238
|
+
const pageText = more.content[0]?.text ?? "";
|
|
239
|
+
text += `\n\n--- page ${++pages} (${next.param} ${next.offset}) ---\n` + pageText;
|
|
240
|
+
next = nextPage(pageText);
|
|
241
|
+
}
|
|
242
|
+
if (followAllPages && next && pages >= MAX_ALL_PAGES) {
|
|
243
|
+
text += `\n\n(--all stopped after ${MAX_ALL_PAGES} pages; continue with --offset ${next.offset})`;
|
|
244
|
+
}
|
|
221
245
|
if (out)
|
|
222
246
|
writeOut(out, text);
|
|
223
247
|
else
|
|
224
248
|
print(text);
|
|
225
249
|
}
|
|
250
|
+
/** Set by main from --all. */
|
|
251
|
+
let followAllPages = false;
|
|
252
|
+
/** The next page a list answer offers ("--offset 40", "--obs-offset 40"), or null. */
|
|
253
|
+
function nextPage(text) {
|
|
254
|
+
const obs = [...text.matchAll(/--obs-offset (\d+)/g)].pop();
|
|
255
|
+
if (obs)
|
|
256
|
+
return { param: "observations_offset", offset: Number(obs[1]) };
|
|
257
|
+
const m = [...text.matchAll(/(?<![-\w])--offset (\d+)/g)].pop();
|
|
258
|
+
return m ? { param: "offset", offset: Number(m[1]) } : null;
|
|
259
|
+
}
|
|
226
260
|
// A `--channel` name that doesn't resolve to a real channel must fail loudly
|
|
227
261
|
// in keyword mode, not fall through as an unfiltered corpus-wide query
|
|
228
262
|
// (signal4#85). Keyword-mode search silently drops channel_names server-side
|
|
@@ -411,7 +445,7 @@ const COMMANDS = {
|
|
|
411
445
|
search: {
|
|
412
446
|
tool: "search_quotes",
|
|
413
447
|
description: "Search quotes by topic and/or speaker",
|
|
414
|
-
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_DAYS, FLAG_MODE, { ...FLAG_LIMIT, default: 10 },
|
|
448
|
+
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_DAYS, FLAG_MODE, { ...FLAG_LIMIT, default: 10 }, ...PAGING, FLAG_LANGUAGE, FLAG_COUNTRY,
|
|
415
449
|
{ name: "sort", type: "string", default: "relevance", desc: "relevance | chronological" },
|
|
416
450
|
{ name: "count", type: "boolean", desc: "Return only the match count" },
|
|
417
451
|
{ name: "full-history", type: "boolean", desc: "Search the entire corpus (requires topic + speaker)" },
|
|
@@ -500,7 +534,7 @@ const COMMANDS = {
|
|
|
500
534
|
{ name: "political-lean-min", type: "number", desc: "Min channel political lean (0=left, 1=right)" },
|
|
501
535
|
{ name: "political-lean-max", type: "number", desc: "Max channel political lean (0=left, 1=right)" },
|
|
502
536
|
{ name: "source", type: "string", desc: "Claim source filter" },
|
|
503
|
-
FLAG_DAYS, { ...FLAG_LIMIT, default: 10 },
|
|
537
|
+
FLAG_DAYS, { ...FLAG_LIMIT, default: 10 }, ...PAGING, FLAG_START, FLAG_END, FLAG_COLLECTION, FLAG_OUT, FLAG_JSON],
|
|
504
538
|
run: async (args, json) => {
|
|
505
539
|
const [positionalTopic] = args.positionals;
|
|
506
540
|
const f = args.flags;
|
|
@@ -733,8 +767,8 @@ const COMMANDS = {
|
|
|
733
767
|
},
|
|
734
768
|
"entities": {
|
|
735
769
|
tool: "find_entities",
|
|
736
|
-
description: "
|
|
737
|
-
specs: [{ name: "type", type: "string", desc: "Entity type filter: person | organization | place | subject" }, FLAG_LIMIT, FLAG_JSON],
|
|
770
|
+
description: "Find entities by name (people, orgs, places, subjects); the first row is what entity-claims uses",
|
|
771
|
+
specs: [{ name: "type", type: "string", desc: "Entity type filter: person | organization | place | subject" }, FLAG_LIMIT, ...PAGING, FLAG_JSON],
|
|
738
772
|
run: async (args, json) => {
|
|
739
773
|
const [query] = args.positionals;
|
|
740
774
|
if (!query) {
|
|
@@ -744,17 +778,18 @@ const COMMANDS = {
|
|
|
744
778
|
const f = args.flags;
|
|
745
779
|
const entityType = f.type || undefined;
|
|
746
780
|
const limit = Number(f.limit) || undefined;
|
|
781
|
+
const offset = Number(f.offset) || undefined;
|
|
747
782
|
if (json)
|
|
748
|
-
return printJson(await client.searchEntities({ query, entity_type: entityType, limit }));
|
|
749
|
-
await runHandler("find_entities", { query, entity_type: entityType, limit });
|
|
783
|
+
return printJson(await client.searchEntities({ query, entity_type: entityType, limit, offset }));
|
|
784
|
+
await runHandler("find_entities", { query, entity_type: entityType, limit, offset });
|
|
750
785
|
},
|
|
751
786
|
},
|
|
752
787
|
"entity-claims": {
|
|
753
788
|
tool: "entity_claims",
|
|
754
|
-
description: "
|
|
789
|
+
description: "Most-spread claims about an entity (every row of that name or alias, merges included)",
|
|
755
790
|
specs: [{ name: "sort", type: "string", default: "spread", desc: "Sort: spread | observations | recent" },
|
|
756
791
|
{ name: "stance", type: "string", desc: "Filter by stance: assert | deny | speculate | attribute" },
|
|
757
|
-
FLAG_LIMIT,
|
|
792
|
+
FLAG_LIMIT, ...PAGING, FLAG_COLLECTION, FLAG_JSON],
|
|
758
793
|
run: async (args, json) => {
|
|
759
794
|
const [name] = args.positionals;
|
|
760
795
|
if (!name) {
|
|
@@ -796,13 +831,14 @@ const COMMANDS = {
|
|
|
796
831
|
specs: [{ name: "metric", type: "string", default: "spread", desc: "Ranking: spread (reach) | contested (assert vs deny balance)" },
|
|
797
832
|
{ name: "category", type: "string", desc: "Optional topic-category filter" },
|
|
798
833
|
{ name: "min-obs", type: "number", default: 2, desc: "Minimum observation count per claim" }, FLAG_DAYS, FLAG_START, FLAG_END,
|
|
799
|
-
FLAG_LANGUAGE, FLAG_COLLECTION, FLAG_LIMIT, FLAG_JSON],
|
|
834
|
+
FLAG_LANGUAGE, FLAG_COLLECTION, FLAG_LIMIT, ...PAGING, FLAG_JSON],
|
|
800
835
|
run: async (args, json) => {
|
|
801
836
|
const f = args.flags;
|
|
802
837
|
const params = {
|
|
803
838
|
metric: f.metric || undefined, category: f.category || undefined,
|
|
804
839
|
days: Number(f.days) || undefined, min_observations: Number(f["min-obs"]) || undefined,
|
|
805
|
-
limit: Number(f.limit) || undefined,
|
|
840
|
+
limit: Number(f.limit) || undefined, offset: Number(f.offset) || undefined,
|
|
841
|
+
language: f.language || undefined,
|
|
806
842
|
...dateFlags(f),
|
|
807
843
|
};
|
|
808
844
|
if (json)
|
|
@@ -813,7 +849,10 @@ const COMMANDS = {
|
|
|
813
849
|
claim: {
|
|
814
850
|
tool: "get_claim",
|
|
815
851
|
description: "Get full claim detail with observations and related claims",
|
|
816
|
-
specs: [{ name: "observations", type: "boolean", desc: "Include the
|
|
852
|
+
specs: [{ name: "observations", type: "boolean", desc: "Include the observation list (20 a page)" },
|
|
853
|
+
{ name: "obs-offset", type: "number", desc: "Observations: skip this many (implies --observations)" },
|
|
854
|
+
{ name: "obs-limit", type: "number", desc: "Observations per page (max 100)" },
|
|
855
|
+
FLAG_ALL,
|
|
817
856
|
{ name: "related", type: "boolean", desc: "Include semantically related claims" }, FLAG_JSON],
|
|
818
857
|
run: async (args, json) => {
|
|
819
858
|
const [id] = args.positionals;
|
|
@@ -822,17 +861,22 @@ const COMMANDS = {
|
|
|
822
861
|
process.exit(1);
|
|
823
862
|
}
|
|
824
863
|
const claimId = Number(id);
|
|
864
|
+
const obsOffset = args.flags["obs-offset"] != null ? Number(args.flags["obs-offset"]) : undefined;
|
|
865
|
+
const obsLimit = args.flags["obs-limit"] != null ? Number(args.flags["obs-limit"]) : undefined;
|
|
825
866
|
if (json) {
|
|
826
867
|
const detail = await client.getClaim(claimId);
|
|
827
|
-
if (args.flags.observations)
|
|
828
|
-
detail.observations = await client.getClaimObservations({ claim_id: claimId });
|
|
868
|
+
if (args.flags.observations || obsOffset != null || obsLimit != null) {
|
|
869
|
+
detail.observations = await client.getClaimObservations({ claim_id: claimId, limit: obsLimit, offset: obsOffset });
|
|
870
|
+
}
|
|
829
871
|
if (args.flags.related)
|
|
830
872
|
detail.related = await client.getRelatedClaims(claimId);
|
|
831
873
|
return printJson(detail);
|
|
832
874
|
}
|
|
833
875
|
await runHandler("get_claim", {
|
|
834
876
|
claim_id: claimId,
|
|
835
|
-
include_observations: args.flags.observations,
|
|
877
|
+
include_observations: args.flags.observations || followAllPages,
|
|
878
|
+
observations_offset: obsOffset,
|
|
879
|
+
observations_limit: obsLimit,
|
|
836
880
|
include_related: args.flags.related,
|
|
837
881
|
});
|
|
838
882
|
},
|
|
@@ -933,7 +977,7 @@ const COMMANDS = {
|
|
|
933
977
|
{ name: "vs", type: "string", desc: "More topics to count side by side, comma-separated (up to 5 in all)" },
|
|
934
978
|
{ name: "order", type: "string", default: "segments", desc: "Episode breakdown order: segments | date" },
|
|
935
979
|
{ name: "min-similarity", type: "number", desc: "Semantic threshold (default 0.35)" },
|
|
936
|
-
{ ...FLAG_LIMIT, desc: "Rows for channel/episode/... breakdowns (-n, -k; max 1000)" },
|
|
980
|
+
{ ...FLAG_LIMIT, desc: "Rows for channel/episode/... breakdowns (-n, -k; max 1000)" }, ...PAGING, FLAG_JSON],
|
|
937
981
|
run: async (args, json) => {
|
|
938
982
|
const f = args.flags;
|
|
939
983
|
const topic = args.positionals.join(" ").trim() || undefined;
|
|
@@ -971,7 +1015,7 @@ const COMMANDS = {
|
|
|
971
1015
|
speaker: {
|
|
972
1016
|
tool: "find_speakers",
|
|
973
1017
|
description: "Speaker search, profile, or discourse",
|
|
974
|
-
specs: [FLAG_DAYS, FLAG_LIMIT,
|
|
1018
|
+
specs: [FLAG_DAYS, FLAG_LIMIT, ...PAGING, FLAG_CHANNEL,
|
|
975
1019
|
{ name: "segments", type: "boolean", desc: "List the speaker's recent segments (chronological)" }, FLAG_JSON],
|
|
976
1020
|
run: async (args, json) => {
|
|
977
1021
|
const [identity, topic] = args.positionals;
|
|
@@ -1045,7 +1089,7 @@ const COMMANDS = {
|
|
|
1045
1089
|
channel: {
|
|
1046
1090
|
tool: "get_channel",
|
|
1047
1091
|
description: "Channel profile or episode listing",
|
|
1048
|
-
specs: [{ name: "episodes", type: "boolean", desc: "List the channel's recent episodes instead of the profile" }, FLAG_DAYS, FLAG_LIMIT,
|
|
1092
|
+
specs: [{ name: "episodes", type: "boolean", desc: "List the channel's recent episodes instead of the profile" }, FLAG_DAYS, FLAG_LIMIT, ...PAGING, FLAG_JSON],
|
|
1049
1093
|
run: async (args, json) => {
|
|
1050
1094
|
const [id] = args.positionals;
|
|
1051
1095
|
if (!id) {
|
|
@@ -1378,26 +1422,38 @@ const COMMANDS = {
|
|
|
1378
1422
|
},
|
|
1379
1423
|
"top-channels": {
|
|
1380
1424
|
tool: "get_top_channels",
|
|
1381
|
-
description: "Top channels by episode count",
|
|
1382
|
-
specs: [FLAG_DAYS, FLAG_START, FLAG_END, FLAG_LIMIT, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_COLLECTION,
|
|
1425
|
+
description: "Top channels by episode count; with --topic, by how much they say about it",
|
|
1426
|
+
specs: [FLAG_DAYS, FLAG_START, FLAG_END, FLAG_LIMIT, ...PAGING, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_COLLECTION,
|
|
1427
|
+
{ name: "topic", type: "string", desc: "Rank by matching segments per channel, from the transcripts" },
|
|
1428
|
+
{ name: "lean", type: "string", desc: "Channel lean: left | center-left | center | center-right | right | 0.0-0.5" },
|
|
1429
|
+
FLAG_JSON],
|
|
1383
1430
|
run: async (args, json) => {
|
|
1384
1431
|
const f = args.flags;
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
|
|
1432
|
+
const topic = f.topic || args.positionals.join(" ") || undefined;
|
|
1433
|
+
const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 20, offset: Number(f.offset) || undefined, language: f.language || undefined, country: f.country || undefined, topic, political_lean: f.lean || undefined, ...dateFlags(f) };
|
|
1434
|
+
if (json) {
|
|
1435
|
+
const lean = parsePoliticalLean(params.political_lean);
|
|
1436
|
+
return printJson(await client.getTopChannels({ ...params, political_lean_min: lean.min, political_lean_max: lean.max, project: f.collection || undefined }));
|
|
1437
|
+
}
|
|
1388
1438
|
await runHandler("get_top_channels", params);
|
|
1389
1439
|
},
|
|
1390
1440
|
},
|
|
1391
1441
|
"top-speakers": {
|
|
1392
1442
|
tool: "get_top_speakers",
|
|
1393
|
-
description: "Top speakers by appearances, talk time
|
|
1443
|
+
description: "Top speakers by appearances, talk time or clips; with --topic, by how much they talk about it",
|
|
1394
1444
|
specs: [FLAG_DAYS, FLAG_START, FLAG_END, FLAG_LIMIT, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_COLLECTION,
|
|
1395
|
-
{ name: "ranking", type: "string", default: "appearances", desc: "Rank by: appearances | talkative (talk time) | clipped" },
|
|
1445
|
+
{ name: "ranking", type: "string", default: "appearances", desc: "Rank by: appearances | talkative (talk time) | clipped" },
|
|
1446
|
+
{ name: "topic", type: "string", desc: "Rank by matching segments each speaker speaks in, from the transcripts (overrides --ranking)" },
|
|
1447
|
+
{ name: "lean", type: "string", desc: "Channel lean: left | center-left | center | center-right | right | 0.0-0.5" },
|
|
1448
|
+
...PAGING, FLAG_JSON],
|
|
1396
1449
|
run: async (args, json) => {
|
|
1397
1450
|
const f = args.flags;
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1451
|
+
const topic = f.topic || args.positionals.join(" ") || undefined;
|
|
1452
|
+
const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 20, offset: Number(f.offset) || undefined, ranking: f.ranking, language: f.language || undefined, country: f.country || undefined, project: f.collection || undefined, topic, political_lean: f.lean || undefined, ...dateFlags(f) };
|
|
1453
|
+
if (json) {
|
|
1454
|
+
const lean = parsePoliticalLean(params.political_lean);
|
|
1455
|
+
return printJson(await client.getTopSpeakers({ ...params, political_lean_min: lean.min, political_lean_max: lean.max }));
|
|
1456
|
+
}
|
|
1401
1457
|
await runHandler("get_top_speakers", params);
|
|
1402
1458
|
},
|
|
1403
1459
|
},
|
|
@@ -1438,7 +1494,7 @@ const COMMANDS = {
|
|
|
1438
1494
|
ads: {
|
|
1439
1495
|
tool: "get_ad_analytics",
|
|
1440
1496
|
description: "Ad intelligence",
|
|
1441
|
-
specs: [FLAG_DAYS, FLAG_LIMIT,
|
|
1497
|
+
specs: [FLAG_DAYS, FLAG_LIMIT, ...PAGING,
|
|
1442
1498
|
{ name: "advertisers", type: "boolean", desc: "List advertisers instead of the overview" },
|
|
1443
1499
|
{ name: "trending", type: "boolean", desc: "Show growing/declining/new advertisers" },
|
|
1444
1500
|
FLAG_CHANNEL,
|
|
@@ -1496,7 +1552,7 @@ const COMMANDS = {
|
|
|
1496
1552
|
description: "Manage bookmarks",
|
|
1497
1553
|
specs: [{ name: "type", type: "string", desc: "Filter by bookmark type: segment | episode | speaker | channel" },
|
|
1498
1554
|
{ name: "search", type: "string", desc: "Filter bookmarks by search text" },
|
|
1499
|
-
FLAG_LIMIT,
|
|
1555
|
+
FLAG_LIMIT, ...PAGING,
|
|
1500
1556
|
{ name: "note", type: "string", desc: "Note to attach when adding a bookmark" }, FLAG_JSON],
|
|
1501
1557
|
run: async (args, json) => {
|
|
1502
1558
|
const [sub, entityType, entityId] = args.positionals;
|
|
@@ -1814,6 +1870,20 @@ export async function main() {
|
|
|
1814
1870
|
}
|
|
1815
1871
|
const json = args.flags.json;
|
|
1816
1872
|
activeCollection = args.flags.collection || undefined;
|
|
1873
|
+
// --page N is --offset (N-1)*limit; --all follows the next-page hint (signal4#329).
|
|
1874
|
+
const page = Number(args.flags.page);
|
|
1875
|
+
if (page) {
|
|
1876
|
+
if (page < 1 || !Number.isInteger(page)) {
|
|
1877
|
+
printErr("Error: --page is a whole number from 1.");
|
|
1878
|
+
process.exit(1);
|
|
1879
|
+
}
|
|
1880
|
+
args.flags.offset = (page - 1) * (Number(args.flags.limit) || 20);
|
|
1881
|
+
}
|
|
1882
|
+
followAllPages = !!args.flags.all;
|
|
1883
|
+
if (followAllPages && json) {
|
|
1884
|
+
printErr("Error: --all pages the text output. For JSON, page with --offset, or use paginate() from @signal4/s4/client.");
|
|
1885
|
+
process.exit(1);
|
|
1886
|
+
}
|
|
1817
1887
|
// --days has no ceiling where the backend takes dates; report endpoints cap at 365.
|
|
1818
1888
|
const days = args.flags.days;
|
|
1819
1889
|
if (typeof days === "number" && days > 365 && !args.flags.start && !DATE_RANGE_COMMANDS.has(command)) {
|