@signal4/s4 2.6.0 → 2.7.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 +33 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +317 -175
- package/dist/cli.js.map +1 -1
- package/dist/cli_args.d.ts +24 -0
- package/dist/cli_args.d.ts.map +1 -0
- package/dist/cli_args.js +130 -0
- package/dist/cli_args.js.map +1 -0
- package/dist/client.d.ts +80 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +128 -24
- package/dist/client.js.map +1 -1
- package/dist/format.d.ts +81 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +209 -47
- package/dist/format.js.map +1 -1
- package/dist/handler_types.d.ts +10 -0
- package/dist/handler_types.d.ts.map +1 -1
- package/dist/handler_types.js +25 -0
- package/dist/handler_types.js.map +1 -1
- package/dist/handlers/ads.js +4 -4
- package/dist/handlers/ads.js.map +1 -1
- package/dist/handlers/analyze.d.ts.map +1 -1
- package/dist/handlers/analyze.js +22 -6
- package/dist/handlers/analyze.js.map +1 -1
- package/dist/handlers/bookmarks.js +2 -2
- package/dist/handlers/bookmarks.js.map +1 -1
- package/dist/handlers/claim_detail.js +2 -2
- package/dist/handlers/claim_detail.js.map +1 -1
- package/dist/handlers/claims.d.ts.map +1 -1
- package/dist/handlers/claims.js +81 -26
- package/dist/handlers/claims.js.map +1 -1
- package/dist/handlers/count.d.ts +7 -0
- package/dist/handlers/count.d.ts.map +1 -0
- package/dist/handlers/count.js +146 -0
- package/dist/handlers/count.js.map +1 -0
- package/dist/handlers/explore.d.ts.map +1 -1
- package/dist/handlers/explore.js +53 -14
- package/dist/handlers/explore.js.map +1 -1
- package/dist/handlers/registry.d.ts.map +1 -1
- package/dist/handlers/registry.js +5 -1
- package/dist/handlers/registry.js.map +1 -1
- package/dist/handlers/reports.d.ts +1 -0
- package/dist/handlers/reports.d.ts.map +1 -1
- package/dist/handlers/reports.js +47 -11
- package/dist/handlers/reports.js.map +1 -1
- package/dist/handlers/search.d.ts.map +1 -1
- package/dist/handlers/search.js +70 -22
- package/dist/handlers/search.js.map +1 -1
- package/dist/handlers/timeline.d.ts.map +1 -1
- package/dist/handlers/timeline.js +14 -8
- package/dist/handlers/timeline.js.map +1 -1
- package/dist/http.js +25 -3
- package/dist/http.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/provenance.d.ts +29 -2
- package/dist/provenance.d.ts.map +1 -1
- package/dist/provenance.js +63 -4
- package/dist/provenance.js.map +1 -1
- package/dist/server_core.d.ts +8 -1
- package/dist/server_core.d.ts.map +1 -1
- package/dist/server_core.js +90 -8
- package/dist/server_core.js.map +1 -1
- package/dist/speed.d.ts +40 -0
- package/dist/speed.d.ts.map +1 -0
- package/dist/speed.js +196 -0
- package/dist/speed.js.map +1 -0
- package/dist/surface.d.ts +7 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +18 -0
- package/dist/surface.js.map +1 -0
- package/dist/types.d.ts +155 -8
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1,105 +1,57 @@
|
|
|
1
1
|
/** Signal4 CLI — terminal interface sharing handlers with the MCP server. */
|
|
2
2
|
import { applyConfig } from "./config.js";
|
|
3
3
|
import { HANDLERS } from "./handlers/registry.js";
|
|
4
|
+
import { descriptionTag, estimate, startNotice, REPORT_ELAPSED_OVER_S } from "./speed.js";
|
|
4
5
|
import * as client from "./client.js";
|
|
5
6
|
import { writeFileSync, mkdirSync, existsSync, chmodSync, readFileSync, statSync } from "node:fs";
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
import { formatDossier } from "./format.js";
|
|
8
9
|
import { homedir } from "node:os";
|
|
9
10
|
import { createInterface } from "node:readline/promises";
|
|
11
|
+
import { parseArgs as parseArgsRaw, UsageError, didYouMean } from "./cli_args.js";
|
|
12
|
+
import { setSurface } from "./surface.js";
|
|
13
|
+
/** Parse or exit 1 with the usage error on stderr. */
|
|
10
14
|
function parseArgs(argv, specs) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// Apply defaults. Flags without a declared default stay undefined —
|
|
14
|
-
// pre-filling numbers with 0 turns optional filters into active ones
|
|
15
|
-
// (political-lean-min=0 + political-lean-max=0 matched zero claims).
|
|
16
|
-
for (const s of specs) {
|
|
17
|
-
if (s.default !== undefined)
|
|
18
|
-
flags[s.name] = s.default;
|
|
19
|
-
else if (s.type === "boolean")
|
|
20
|
-
flags[s.name] = false;
|
|
21
|
-
}
|
|
22
|
-
// Build lookup: --name and -alias -> spec
|
|
23
|
-
const lookup = new Map();
|
|
24
|
-
for (const s of specs) {
|
|
25
|
-
lookup.set(`--${s.name}`, s);
|
|
26
|
-
if (s.alias)
|
|
27
|
-
lookup.set(`-${s.alias}`, s);
|
|
28
|
-
}
|
|
29
|
-
let i = 0;
|
|
30
|
-
while (i < argv.length) {
|
|
31
|
-
const arg = argv[i];
|
|
32
|
-
const spec = lookup.get(arg);
|
|
33
|
-
if (spec) {
|
|
34
|
-
if (spec.type === "boolean") {
|
|
35
|
-
flags[spec.name] = true;
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
const val = argv[++i];
|
|
39
|
-
if (spec.type === "number") {
|
|
40
|
-
const num = Number(val);
|
|
41
|
-
if (val === undefined || val === "" || Number.isNaN(num)) {
|
|
42
|
-
process.stderr.write(`Error: --${spec.name} expects a number, got "${val ?? ""}".\n`);
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
flags[spec.name] = num;
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
flags[spec.name] = val ?? "";
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
else if (arg.startsWith("-") && arg !== "-") {
|
|
53
|
-
// Hard error, not a warning: a silently-dropped flag falls through to
|
|
54
|
-
// default behavior, and the caller reads the wrong answer as the right one.
|
|
55
|
-
const valid = specs.map((s) => `--${s.name}${s.alias ? ` (-${s.alias})` : ""}`).join(", ");
|
|
56
|
-
process.stderr.write(`Error: unknown flag "${arg}" for this command.\nValid flags: ${valid}\nRun with --help for details.\n`);
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
positionals.push(arg);
|
|
61
|
-
}
|
|
62
|
-
i++;
|
|
63
|
-
}
|
|
64
|
-
// Post-parse validation for shared flags — a silently NaN-coerced --days or
|
|
65
|
-
// a raw backend error for a malformed date both read as broken tooling.
|
|
66
|
-
const days = flags.days;
|
|
67
|
-
if (typeof days === "number" && specs.some((s) => s.name === "days")) {
|
|
68
|
-
if (!Number.isInteger(days) || days < 1) {
|
|
69
|
-
process.stderr.write(`Error: --days must be a positive integer (got "${days}").\n`);
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
if (days > 365) {
|
|
73
|
-
process.stderr.write(`Error: --days is capped at 365. Use --start YYYY-MM-DD for older material (s4 search also supports --full-history).\n`);
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
15
|
+
try {
|
|
16
|
+
return parseArgsRaw(argv, specs);
|
|
76
17
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
process.stderr.write(`Error: --${name} must be YYYY-MM-DD (e.g. 2026-07-13), got "${v}".\n`);
|
|
18
|
+
catch (err) {
|
|
19
|
+
if (err instanceof UsageError) {
|
|
20
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
81
21
|
process.exit(1);
|
|
82
22
|
}
|
|
23
|
+
throw err;
|
|
83
24
|
}
|
|
84
|
-
return { positionals, flags };
|
|
85
25
|
}
|
|
86
26
|
// ---------------------------------------------------------------------------
|
|
87
27
|
// Shared flag specs
|
|
88
28
|
// ---------------------------------------------------------------------------
|
|
89
|
-
const FLAG_DAYS = { name: "days", alias: "d", type: "number", default: 30, desc: "Time window in days (
|
|
90
|
-
const FLAG_LIMIT = { name: "limit", alias: "n", type: "number", default: 20, desc: "
|
|
91
|
-
const FLAG_OFFSET = { name: "offset",
|
|
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
|
+
const FLAG_LIMIT = { name: "limit", alias: ["n", "k"], type: "number", default: 20, desc: "Number of results (-n, -k)" };
|
|
31
|
+
const FLAG_OFFSET = { name: "offset", type: "number", default: 0, desc: "Skip this many results (pagination)" };
|
|
92
32
|
const FLAG_SPEAKER = { name: "speaker", alias: "s", type: "string", desc: "Filter by speaker name (full name)" };
|
|
93
33
|
const FLAG_CHANNEL = { name: "channel", alias: "c", type: "string", desc: "Filter by channel name (comma-separated for multiple)" };
|
|
94
|
-
const FLAG_LANGUAGE = { name: "language",
|
|
34
|
+
const FLAG_LANGUAGE = { name: "language", type: "string", desc: "Filter by language code (en, fr, ...)" };
|
|
95
35
|
const FLAG_COUNTRY = { name: "country", type: "string", desc: "Filter by channel country code" };
|
|
36
|
+
const FLAG_COLLECTION = { name: "collection", long: ["project"], type: "string", desc: "Restrict to one collection (see: s4 collections). Default: whole corpus" };
|
|
96
37
|
const FLAG_MODE = { name: "mode", alias: "m", type: "string", default: "semantic", desc: "Search mode: semantic | keyword | hybrid" };
|
|
97
|
-
const FLAG_START = { name: "start", type: "string", desc: "Start date YYYY-MM-DD (
|
|
98
|
-
const FLAG_END = { name: "end", type: "string", desc: "End date YYYY-MM-DD (default today)" };
|
|
99
|
-
const FLAG_KEYWORD = { name: "keyword",
|
|
38
|
+
const FLAG_START = { name: "start", long: ["since"], type: "string", desc: "Start date: YYYY, YYYY-MM or YYYY-MM-DD (overrides --days)" };
|
|
39
|
+
const FLAG_END = { name: "end", long: ["until"], type: "string", desc: "End date: YYYY, YYYY-MM or YYYY-MM-DD (default today)" };
|
|
40
|
+
const FLAG_KEYWORD = { name: "keyword", type: "string", desc: "Exact phrase search (implies --mode keyword)" };
|
|
100
41
|
const FLAG_SKIP_CACHE = { name: "skip-cache", type: "boolean", desc: "Force fresh analysis (bypass cache)" };
|
|
101
|
-
const FLAG_OUT = { name: "out", type: "string", desc: "Write the rendered output to a file (.md, or .csv on search)" };
|
|
42
|
+
const FLAG_OUT = { name: "out", alias: "o", long: ["output"], type: "string", desc: "Write the rendered output to a file (.md, or .csv on search)" };
|
|
102
43
|
const FLAG_JSON = { name: "json", type: "boolean", desc: "Raw JSON output" };
|
|
44
|
+
const GRANULARITIES = ["day", "week", "month", "quarter", "year"];
|
|
45
|
+
const COUNT_GROUPS = ["none", ...GRANULARITIES, "channel", "episode", "language", "country", "platform", "collection", "lean"];
|
|
46
|
+
/** --start/--end as tool params (already normalized to YYYY-MM-DD by parseArgs); absent when not given. */
|
|
47
|
+
function dateFlags(f) {
|
|
48
|
+
const out = {};
|
|
49
|
+
if (f.start)
|
|
50
|
+
out.start_date = f.start;
|
|
51
|
+
if (f.end)
|
|
52
|
+
out.end_date = f.end;
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
103
55
|
// ---------------------------------------------------------------------------
|
|
104
56
|
// Progress reporting for long-running analysis commands
|
|
105
57
|
// ---------------------------------------------------------------------------
|
|
@@ -210,11 +162,15 @@ function parseSegmentId(raw) {
|
|
|
210
162
|
// Accept all of: N, seg:N, seg_N, {seg_N}, {seg:N}.
|
|
211
163
|
const id = Number(raw.replace(/^\{(.*)\}$/, "$1").replace(/^seg[:_]/, ""));
|
|
212
164
|
if (!Number.isInteger(id)) {
|
|
213
|
-
|
|
165
|
+
printErr(`Error: invalid segment ID "${raw}" (expected a number, seg:<number>, or seg_<number>).`);
|
|
214
166
|
process.exit(1);
|
|
215
167
|
}
|
|
216
168
|
return id;
|
|
217
169
|
}
|
|
170
|
+
function printErr(text) {
|
|
171
|
+
activeProgress?.finish();
|
|
172
|
+
process.stderr.write(text + "\n");
|
|
173
|
+
}
|
|
218
174
|
function print(text) {
|
|
219
175
|
activeProgress?.finish();
|
|
220
176
|
process.stdout.write(text + "\n");
|
|
@@ -238,13 +194,25 @@ function toCsv(rows, columns) {
|
|
|
238
194
|
function printJson(data) {
|
|
239
195
|
print(JSON.stringify(data, null, 2));
|
|
240
196
|
}
|
|
197
|
+
/** The parsed --collection for the running command; injected into every handler call. */
|
|
198
|
+
let activeCollection;
|
|
241
199
|
async function runHandler(tool, params, out) {
|
|
242
200
|
const handler = HANDLERS[tool];
|
|
243
201
|
if (!handler) {
|
|
244
|
-
|
|
202
|
+
printErr(`Error: handler "${tool}" not found.`);
|
|
245
203
|
process.exit(1);
|
|
246
204
|
}
|
|
205
|
+
if (params.collection === undefined && activeCollection)
|
|
206
|
+
params = { ...params, collection: activeCollection };
|
|
207
|
+
// Slow calls say how long to expect before they start; stderr, so stdout stays the answer.
|
|
208
|
+
const expected = estimate(tool, params);
|
|
209
|
+
if (expected.tier === "slow")
|
|
210
|
+
process.stderr.write(`${startNotice(expected)}\n`);
|
|
211
|
+
const started = Date.now();
|
|
247
212
|
const result = await handler(params);
|
|
213
|
+
const took = (Date.now() - started) / 1000;
|
|
214
|
+
if (took > REPORT_ELAPSED_OVER_S)
|
|
215
|
+
process.stderr.write(`Took ${took.toFixed(1)}s.\n`);
|
|
248
216
|
if (result.isError) {
|
|
249
217
|
process.stderr.write((result.content[0]?.text ?? "Unknown error") + "\n");
|
|
250
218
|
process.exit(1);
|
|
@@ -255,6 +223,41 @@ async function runHandler(tool, params, out) {
|
|
|
255
223
|
else
|
|
256
224
|
print(text);
|
|
257
225
|
}
|
|
226
|
+
// A `--channel` name that doesn't resolve to a real channel must fail loudly
|
|
227
|
+
// in keyword mode, not fall through as an unfiltered corpus-wide query
|
|
228
|
+
// (signal4#85). Keyword-mode search silently drops channel_names server-side
|
|
229
|
+
// (a server-side defect, out of scope here) so an unresolvable name there
|
|
230
|
+
// would otherwise read as a channel-scoped count that is actually
|
|
231
|
+
// corpus-wide. Semantic mode is NOT gated: the server already returns an
|
|
232
|
+
// empty result set for an unknown channel there, which is the correct UX —
|
|
233
|
+
// adding a second, client-side source of truth on that path risks disagreeing
|
|
234
|
+
// with the server and regressing it.
|
|
235
|
+
//
|
|
236
|
+
// Resolved against /entities/channels/by-name — the one client-reachable
|
|
237
|
+
// place this can be caught. Multiple names resolve concurrently. A non-404
|
|
238
|
+
// failure (network blip, 5xx, timeout) fails OPEN: warn on stderr and let the
|
|
239
|
+
// search proceed rather than blocking a query that might otherwise succeed.
|
|
240
|
+
async function assertChannelsExistForKeywordMode(channelArg, mode) {
|
|
241
|
+
if (!channelArg || mode !== "keyword")
|
|
242
|
+
return;
|
|
243
|
+
const names = channelArg.split(",").map((s) => s.trim()).filter(Boolean);
|
|
244
|
+
await Promise.all(names.map(async (name) => {
|
|
245
|
+
try {
|
|
246
|
+
await client.getChannelByName(name);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
250
|
+
if (/^HTTP 404\b/.test(msg)) {
|
|
251
|
+
process.stderr.write(`Error: channel "${name}" not found. This checks the channels registry ` +
|
|
252
|
+
`(display_name) only — a name that exists only as a content-level ` +
|
|
253
|
+
`channel_name variant may still be valid; verify against the exact ` +
|
|
254
|
+
`registry name (s4 channel "${name}" or s4 top-channels).\n`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
process.stderr.write(`Warning: could not verify channel "${name}" exists (${msg}) — proceeding without the check.\n`);
|
|
258
|
+
}
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
258
261
|
// ---------------------------------------------------------------------------
|
|
259
262
|
// Batch dossiers (surrogate-roster sweeps): s4 dossier "<topic>" --speakers file.txt --out dir/
|
|
260
263
|
// ---------------------------------------------------------------------------
|
|
@@ -408,21 +411,22 @@ const COMMANDS = {
|
|
|
408
411
|
search: {
|
|
409
412
|
tool: "search_quotes",
|
|
410
413
|
description: "Search quotes by topic and/or speaker",
|
|
411
|
-
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_DAYS, FLAG_MODE, FLAG_LIMIT, FLAG_OFFSET, FLAG_LANGUAGE, FLAG_COUNTRY,
|
|
414
|
+
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_DAYS, FLAG_MODE, { ...FLAG_LIMIT, default: 10 }, FLAG_OFFSET, FLAG_LANGUAGE, FLAG_COUNTRY,
|
|
412
415
|
{ name: "sort", type: "string", default: "relevance", desc: "relevance | chronological" },
|
|
413
416
|
{ name: "count", type: "boolean", desc: "Return only the match count" },
|
|
414
417
|
{ name: "full-history", type: "boolean", desc: "Search the entire corpus (requires topic + speaker)" },
|
|
415
418
|
{ name: "min-similarity", type: "number", desc: "Minimum similarity threshold (0.0-1.0, default 0.40)" },
|
|
416
419
|
FLAG_KEYWORD,
|
|
417
420
|
{ name: "speaker-id", type: "number", desc: "Speaker identity ID (from s4 speaker)" }, FLAG_OUT,
|
|
418
|
-
FLAG_START, FLAG_END,
|
|
421
|
+
FLAG_START, FLAG_END, FLAG_COLLECTION,
|
|
419
422
|
FLAG_JSON],
|
|
420
423
|
run: async (args, json) => {
|
|
421
424
|
const [positionalTopic] = args.positionals;
|
|
422
425
|
const f = args.flags;
|
|
423
426
|
const keyword = f.keyword || undefined;
|
|
424
|
-
const topic =
|
|
427
|
+
const topic = keyword || positionalTopic || undefined;
|
|
425
428
|
const mode = keyword ? "keyword" : f.mode;
|
|
429
|
+
await assertChannelsExistForKeywordMode(f.channel || undefined, mode);
|
|
426
430
|
const days = Number(f.days) || 30;
|
|
427
431
|
const fullHistory = f["full-history"];
|
|
428
432
|
// Full history means FULL history — only pass dates given explicitly,
|
|
@@ -441,6 +445,7 @@ const COMMANDS = {
|
|
|
441
445
|
count_only: f.count, sort: f.sort,
|
|
442
446
|
full_history: fullHistory,
|
|
443
447
|
min_similarity: f["min-similarity"] !== undefined ? Number(f["min-similarity"]) : undefined,
|
|
448
|
+
project: f.collection || undefined,
|
|
444
449
|
};
|
|
445
450
|
const out = f.out;
|
|
446
451
|
if (json || (out && out.endsWith(".csv"))) {
|
|
@@ -455,6 +460,7 @@ const COMMANDS = {
|
|
|
455
460
|
full_history: fullHistory || undefined,
|
|
456
461
|
count_only: params.count_only || undefined,
|
|
457
462
|
min_similarity: params.min_similarity,
|
|
463
|
+
project: params.project,
|
|
458
464
|
});
|
|
459
465
|
if (out && out.endsWith(".csv")) {
|
|
460
466
|
const rows = (data.quotes ?? []).map((q) => ({
|
|
@@ -484,7 +490,7 @@ const COMMANDS = {
|
|
|
484
490
|
claims: {
|
|
485
491
|
tool: "search_claims",
|
|
486
492
|
description: "Search extracted claims",
|
|
487
|
-
specs: [{ name: "entity",
|
|
493
|
+
specs: [{ name: "entity", type: "string", desc: "Filter by entity name (person, org, place, subject)" }, FLAG_SPEAKER, FLAG_CHANNEL,
|
|
488
494
|
{ name: "category", type: "string", desc: "Topic category filter" },
|
|
489
495
|
{ name: "stance", type: "string", desc: "Filter by stance: assert | deny | speculate | attribute" },
|
|
490
496
|
FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_MODE, FLAG_KEYWORD,
|
|
@@ -494,12 +500,12 @@ const COMMANDS = {
|
|
|
494
500
|
{ name: "political-lean-min", type: "number", desc: "Min channel political lean (0=left, 1=right)" },
|
|
495
501
|
{ name: "political-lean-max", type: "number", desc: "Max channel political lean (0=left, 1=right)" },
|
|
496
502
|
{ name: "source", type: "string", desc: "Claim source filter" },
|
|
497
|
-
FLAG_DAYS, FLAG_LIMIT, FLAG_OFFSET, FLAG_START, FLAG_END, FLAG_OUT, FLAG_JSON],
|
|
503
|
+
FLAG_DAYS, { ...FLAG_LIMIT, default: 10 }, FLAG_OFFSET, FLAG_START, FLAG_END, FLAG_COLLECTION, FLAG_OUT, FLAG_JSON],
|
|
498
504
|
run: async (args, json) => {
|
|
499
505
|
const [positionalTopic] = args.positionals;
|
|
500
506
|
const f = args.flags;
|
|
501
507
|
const keyword = f.keyword || undefined;
|
|
502
|
-
const topic =
|
|
508
|
+
const topic = keyword || positionalTopic || undefined;
|
|
503
509
|
const mode = keyword ? "keyword" : f.mode;
|
|
504
510
|
const days = Number(f.days) || 30;
|
|
505
511
|
const dr = f.start ? { start_date: f.start, end_date: f.end || new Date().toISOString().slice(0, 10) } : undefined;
|
|
@@ -520,6 +526,7 @@ const COMMANDS = {
|
|
|
520
526
|
days, mode,
|
|
521
527
|
sort: f.sort, min_observations: Number(f["min-obs"]) || 1,
|
|
522
528
|
limit: Number(f.limit), offset: Number(f.offset),
|
|
529
|
+
project: f.collection || undefined,
|
|
523
530
|
...dr,
|
|
524
531
|
};
|
|
525
532
|
const claimsDr = dr ?? daysRange(days);
|
|
@@ -535,6 +542,7 @@ const COMMANDS = {
|
|
|
535
542
|
mode: params.mode, sort: params.sort,
|
|
536
543
|
min_observations: params.min_observations > 1 ? params.min_observations : undefined,
|
|
537
544
|
limit: params.limit, offset: params.offset,
|
|
545
|
+
project: params.project,
|
|
538
546
|
}));
|
|
539
547
|
await runHandler("search_claims", params, f.out);
|
|
540
548
|
},
|
|
@@ -542,12 +550,12 @@ const COMMANDS = {
|
|
|
542
550
|
"analyze-claims": {
|
|
543
551
|
tool: "analyze_claims",
|
|
544
552
|
description: "Analyze a topic over extracted claims — themes with observation stats (fast, no LLM)",
|
|
545
|
-
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_LANGUAGE, FLAG_DAYS,
|
|
553
|
+
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_LANGUAGE, FLAG_DAYS, FLAG_START, FLAG_END, FLAG_COLLECTION,
|
|
546
554
|
{ name: "min-obs", type: "number", default: 1, desc: "Minimum observation count per claim" }, FLAG_JSON],
|
|
547
555
|
run: async (args, json) => {
|
|
548
556
|
const [topic] = args.positionals;
|
|
549
557
|
if (!topic) {
|
|
550
|
-
|
|
558
|
+
printErr("Error: topic is required.");
|
|
551
559
|
process.exit(1);
|
|
552
560
|
}
|
|
553
561
|
const f = args.flags;
|
|
@@ -559,11 +567,13 @@ const COMMANDS = {
|
|
|
559
567
|
language: f.language || undefined,
|
|
560
568
|
days: Number(f.days) || undefined,
|
|
561
569
|
min_observations: Number(f["min-obs"]) || undefined,
|
|
570
|
+
...dateFlags(f),
|
|
562
571
|
};
|
|
563
572
|
if (json)
|
|
564
573
|
return printJson(await client.analyzeTopicClaims({
|
|
565
574
|
topic, speaker_name: params.speaker, channel_names: params.channels,
|
|
566
575
|
language: params.language, days: params.days, min_observations: params.min_observations,
|
|
576
|
+
project: f.collection || undefined, ...dateFlags(f),
|
|
567
577
|
}));
|
|
568
578
|
await runHandler("analyze_claims", params);
|
|
569
579
|
},
|
|
@@ -573,11 +583,12 @@ const COMMANDS = {
|
|
|
573
583
|
description: "Detect whether a topic is surging/fading by claim volume (fast)",
|
|
574
584
|
specs: [{ name: "recent-days", type: "number", default: 7, desc: "Recent window in days" },
|
|
575
585
|
{ name: "baseline-days", type: "number", default: 30, desc: "Baseline window in days" },
|
|
576
|
-
{ name: "min-similarity", type: "number", desc: "Minimum similarity threshold (0.0-1.0)" },
|
|
586
|
+
{ name: "min-similarity", type: "number", desc: "Minimum similarity threshold (0.0-1.0)" },
|
|
587
|
+
FLAG_LANGUAGE, FLAG_COLLECTION, FLAG_JSON],
|
|
577
588
|
run: async (args, json) => {
|
|
578
589
|
const [topic] = args.positionals;
|
|
579
590
|
if (!topic) {
|
|
580
|
-
|
|
591
|
+
printErr("Error: topic is required.");
|
|
581
592
|
process.exit(1);
|
|
582
593
|
}
|
|
583
594
|
const f = args.flags;
|
|
@@ -586,32 +597,34 @@ const COMMANDS = {
|
|
|
586
597
|
recent_days: Number(f["recent-days"]) || undefined,
|
|
587
598
|
baseline_days: Number(f["baseline-days"]) || undefined,
|
|
588
599
|
min_similarity: f["min-similarity"] !== undefined ? Number(f["min-similarity"]) : undefined,
|
|
600
|
+
language: f.language || undefined,
|
|
589
601
|
};
|
|
590
602
|
if (json)
|
|
591
|
-
return printJson(await client.detectTrendClaims(params));
|
|
603
|
+
return printJson(await client.detectTrendClaims({ ...params, project: f.collection || undefined }));
|
|
592
604
|
await runHandler("trend_claims", params);
|
|
593
605
|
},
|
|
594
606
|
},
|
|
595
607
|
"contradict-claims": {
|
|
596
608
|
tool: "contradict_claims",
|
|
597
609
|
description: "Find a speaker's contradictory claims on a topic via stance analysis (fast)",
|
|
598
|
-
specs: [FLAG_SPEAKER, { ...FLAG_DAYS, default: 365 }, FLAG_OUT, FLAG_JSON],
|
|
610
|
+
specs: [FLAG_SPEAKER, { ...FLAG_DAYS, default: 365 }, FLAG_START, FLAG_END, FLAG_COLLECTION, FLAG_OUT, FLAG_JSON],
|
|
599
611
|
run: async (args, json) => {
|
|
600
612
|
const [topic] = args.positionals;
|
|
601
613
|
if (!topic) {
|
|
602
|
-
|
|
614
|
+
printErr("Error: topic is required.");
|
|
603
615
|
process.exit(1);
|
|
604
616
|
}
|
|
605
617
|
const f = args.flags;
|
|
606
618
|
const speaker = f.speaker || undefined;
|
|
607
619
|
if (!speaker) {
|
|
608
|
-
|
|
620
|
+
printErr("Error: --speaker is required.");
|
|
609
621
|
process.exit(1);
|
|
610
622
|
}
|
|
611
|
-
const params = { topic, speaker, days: Number(f.days) || undefined };
|
|
623
|
+
const params = { topic, speaker, days: Number(f.days) || undefined, ...dateFlags(f) };
|
|
612
624
|
if (json)
|
|
613
625
|
return printJson(await client.checkContradictionsClaims({
|
|
614
626
|
topic, speaker_name: speaker, days: params.days,
|
|
627
|
+
project: f.collection || undefined, ...dateFlags(f),
|
|
615
628
|
}));
|
|
616
629
|
await runHandler("contradict_claims", params, f.out);
|
|
617
630
|
},
|
|
@@ -619,24 +632,26 @@ const COMMANDS = {
|
|
|
619
632
|
"compare-claims": {
|
|
620
633
|
tool: "compare_claims",
|
|
621
634
|
description: "Compare 2-5 speakers' claims on a topic — side-by-side stance (fast)",
|
|
622
|
-
specs: [{ name: "speakers", type: "string", desc: "2-5 comma-separated speaker names" }, FLAG_SPEAKER, { ...FLAG_DAYS, default: 90 },
|
|
635
|
+
specs: [{ name: "speakers", type: "string", desc: "2-5 comma-separated speaker names" }, FLAG_SPEAKER, { ...FLAG_DAYS, default: 90 },
|
|
636
|
+
FLAG_START, FLAG_END, FLAG_LANGUAGE, FLAG_COLLECTION, FLAG_OUT, FLAG_JSON],
|
|
623
637
|
run: async (args, json) => {
|
|
624
638
|
const [topic] = args.positionals;
|
|
625
639
|
if (!topic) {
|
|
626
|
-
|
|
640
|
+
printErr("Error: topic is required.");
|
|
627
641
|
process.exit(1);
|
|
628
642
|
}
|
|
629
643
|
const f = args.flags;
|
|
630
644
|
const speakersStr = f.speakers || f.speaker || "";
|
|
631
645
|
const speakers = speakersStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
632
646
|
if (speakers.length < 2) {
|
|
633
|
-
|
|
647
|
+
printErr("Error: --speakers requires 2-5 comma-separated names.");
|
|
634
648
|
process.exit(1);
|
|
635
649
|
}
|
|
636
|
-
const params = { topic, speakers, days: Number(f.days) || undefined };
|
|
650
|
+
const params = { topic, speakers, days: Number(f.days) || undefined, language: f.language || undefined, ...dateFlags(f) };
|
|
637
651
|
if (json)
|
|
638
652
|
return printJson(await client.compareSpeakersClaims({
|
|
639
|
-
topic, speaker_names: speakers, days: params.days,
|
|
653
|
+
topic, speaker_names: speakers, days: params.days, language: params.language,
|
|
654
|
+
project: f.collection || undefined, ...dateFlags(f),
|
|
640
655
|
}));
|
|
641
656
|
await runHandler("compare_claims", params, f.out);
|
|
642
657
|
},
|
|
@@ -644,24 +659,24 @@ const COMMANDS = {
|
|
|
644
659
|
"channel-dossier": {
|
|
645
660
|
tool: "channel_dossier",
|
|
646
661
|
description: "Claims dossier of what a channel (show) platformed on a topic (fast)",
|
|
647
|
-
specs: [FLAG_CHANNEL, { ...FLAG_DAYS, default: 180 }, FLAG_OUT, FLAG_JSON],
|
|
662
|
+
specs: [FLAG_CHANNEL, { ...FLAG_DAYS, default: 180 }, FLAG_COLLECTION, FLAG_OUT, FLAG_JSON],
|
|
648
663
|
run: async (args, json) => {
|
|
649
664
|
const [topic] = args.positionals;
|
|
650
665
|
if (!topic) {
|
|
651
|
-
|
|
666
|
+
printErr("Error: topic is required.");
|
|
652
667
|
process.exit(1);
|
|
653
668
|
}
|
|
654
669
|
const f = args.flags;
|
|
655
670
|
const channel = f.channel || undefined;
|
|
656
671
|
if (!channel) {
|
|
657
|
-
|
|
672
|
+
printErr("Error: --channel is required.");
|
|
658
673
|
process.exit(1);
|
|
659
674
|
}
|
|
660
675
|
const channels = channel.split(",").map((s) => s.trim()).filter(Boolean);
|
|
661
676
|
const days = Number(f.days) || undefined;
|
|
662
677
|
if (json)
|
|
663
678
|
return printJson(await client.buildChannelDossier({
|
|
664
|
-
topic, channel_names: channels, days,
|
|
679
|
+
topic, channel_names: channels, days, project: f.collection || undefined,
|
|
665
680
|
}));
|
|
666
681
|
await runHandler("channel_dossier", { topic, channels, days }, f.out);
|
|
667
682
|
},
|
|
@@ -669,24 +684,24 @@ const COMMANDS = {
|
|
|
669
684
|
"compare-channels": {
|
|
670
685
|
tool: "compare_channels",
|
|
671
686
|
description: "Compare 2-5 channels' claims on a topic — stance + speaker overlap (fast)",
|
|
672
|
-
specs: [{ name: "channels", type: "string", desc: "2-5 comma-separated channel names" }, FLAG_CHANNEL, { ...FLAG_DAYS, default: 90 }, FLAG_OUT, FLAG_JSON],
|
|
687
|
+
specs: [{ name: "channels", type: "string", desc: "2-5 comma-separated channel names" }, FLAG_CHANNEL, { ...FLAG_DAYS, default: 90 }, FLAG_COLLECTION, FLAG_OUT, FLAG_JSON],
|
|
673
688
|
run: async (args, json) => {
|
|
674
689
|
const [topic] = args.positionals;
|
|
675
690
|
if (!topic) {
|
|
676
|
-
|
|
691
|
+
printErr("Error: topic is required.");
|
|
677
692
|
process.exit(1);
|
|
678
693
|
}
|
|
679
694
|
const f = args.flags;
|
|
680
695
|
const channelsStr = f.channels || f.channel || "";
|
|
681
696
|
const channels = channelsStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
682
697
|
if (channels.length < 2) {
|
|
683
|
-
|
|
698
|
+
printErr("Error: --channels requires 2-5 comma-separated names.");
|
|
684
699
|
process.exit(1);
|
|
685
700
|
}
|
|
686
701
|
const days = Number(f.days) || undefined;
|
|
687
702
|
if (json)
|
|
688
703
|
return printJson(await client.compareChannelsClaims({
|
|
689
|
-
topic, channel_names: channels, days,
|
|
704
|
+
topic, channel_names: channels, days, project: f.collection || undefined,
|
|
690
705
|
}));
|
|
691
706
|
await runHandler("compare_channels", { topic, channels, days }, f.out);
|
|
692
707
|
},
|
|
@@ -694,24 +709,24 @@ const COMMANDS = {
|
|
|
694
709
|
"channel-contradictions": {
|
|
695
710
|
tool: "check_channel_contradictions",
|
|
696
711
|
description: "Contradictory claims a channel platforms on a topic (fast)",
|
|
697
|
-
specs: [FLAG_CHANNEL, { ...FLAG_DAYS, default: 365 }, FLAG_OUT, FLAG_JSON],
|
|
712
|
+
specs: [FLAG_CHANNEL, { ...FLAG_DAYS, default: 365 }, FLAG_COLLECTION, FLAG_OUT, FLAG_JSON],
|
|
698
713
|
run: async (args, json) => {
|
|
699
714
|
const [topic] = args.positionals;
|
|
700
715
|
if (!topic) {
|
|
701
|
-
|
|
716
|
+
printErr("Error: topic is required.");
|
|
702
717
|
process.exit(1);
|
|
703
718
|
}
|
|
704
719
|
const f = args.flags;
|
|
705
720
|
const channel = f.channel || undefined;
|
|
706
721
|
if (!channel) {
|
|
707
|
-
|
|
722
|
+
printErr("Error: --channel is required.");
|
|
708
723
|
process.exit(1);
|
|
709
724
|
}
|
|
710
725
|
const channels = channel.split(",").map((s) => s.trim()).filter(Boolean);
|
|
711
726
|
const days = Number(f.days) || undefined;
|
|
712
727
|
if (json)
|
|
713
728
|
return printJson(await client.checkChannelContradictionsClaims({
|
|
714
|
-
topic, channel_names: channels, days,
|
|
729
|
+
topic, channel_names: channels, days, project: f.collection || undefined,
|
|
715
730
|
}));
|
|
716
731
|
await runHandler("check_channel_contradictions", { topic, channels, days }, f.out);
|
|
717
732
|
},
|
|
@@ -723,7 +738,7 @@ const COMMANDS = {
|
|
|
723
738
|
run: async (args, json) => {
|
|
724
739
|
const [query] = args.positionals;
|
|
725
740
|
if (!query) {
|
|
726
|
-
|
|
741
|
+
printErr("Error: query is required.");
|
|
727
742
|
process.exit(1);
|
|
728
743
|
}
|
|
729
744
|
const f = args.flags;
|
|
@@ -739,11 +754,11 @@ const COMMANDS = {
|
|
|
739
754
|
description: "Claims about a resolved canonical entity, ranked, with stance distribution",
|
|
740
755
|
specs: [{ name: "sort", type: "string", default: "spread", desc: "Sort: spread | observations | recent" },
|
|
741
756
|
{ name: "stance", type: "string", desc: "Filter by stance: assert | deny | speculate | attribute" },
|
|
742
|
-
FLAG_LIMIT, FLAG_OFFSET, FLAG_JSON],
|
|
757
|
+
FLAG_LIMIT, FLAG_OFFSET, FLAG_COLLECTION, FLAG_JSON],
|
|
743
758
|
run: async (args, json) => {
|
|
744
759
|
const [name] = args.positionals;
|
|
745
760
|
if (!name) {
|
|
746
|
-
|
|
761
|
+
printErr("Error: entity name is required.");
|
|
747
762
|
process.exit(1);
|
|
748
763
|
}
|
|
749
764
|
const f = args.flags;
|
|
@@ -754,6 +769,7 @@ const COMMANDS = {
|
|
|
754
769
|
if (json)
|
|
755
770
|
return printJson(await client.getEntityClaims({
|
|
756
771
|
name, sort: params.sort, stance: params.stance, limit: params.limit, offset: params.offset,
|
|
772
|
+
project: f.collection || undefined,
|
|
757
773
|
}));
|
|
758
774
|
await runHandler("entity_claims", params);
|
|
759
775
|
},
|
|
@@ -761,16 +777,16 @@ const COMMANDS = {
|
|
|
761
777
|
"claim-graph": {
|
|
762
778
|
tool: "claim_graph",
|
|
763
779
|
description: "A claim's parent, children, and sibling (variant) claims",
|
|
764
|
-
specs: [FLAG_JSON],
|
|
780
|
+
specs: [FLAG_COLLECTION, FLAG_JSON],
|
|
765
781
|
run: async (args, json) => {
|
|
766
782
|
const [id] = args.positionals;
|
|
767
783
|
if (!id || !/^\d+$/.test(id)) {
|
|
768
|
-
|
|
784
|
+
printErr("Error: claim_id (numeric) required.");
|
|
769
785
|
process.exit(1);
|
|
770
786
|
}
|
|
771
787
|
const claimId = Number(id);
|
|
772
788
|
if (json)
|
|
773
|
-
return printJson(await client.getClaimGraph(claimId));
|
|
789
|
+
return printJson(await client.getClaimGraph(claimId, args.flags.collection || undefined));
|
|
774
790
|
await runHandler("claim_graph", { claim_id: claimId });
|
|
775
791
|
},
|
|
776
792
|
},
|
|
@@ -779,16 +795,18 @@ const COMMANDS = {
|
|
|
779
795
|
description: "Leaderboard of the most widely-spread or most-contested claims",
|
|
780
796
|
specs: [{ name: "metric", type: "string", default: "spread", desc: "Ranking: spread (reach) | contested (assert vs deny balance)" },
|
|
781
797
|
{ name: "category", type: "string", desc: "Optional topic-category filter" },
|
|
782
|
-
{ name: "min-obs", type: "number", default: 2, desc: "Minimum observation count per claim" }, FLAG_DAYS,
|
|
798
|
+
{ 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],
|
|
783
800
|
run: async (args, json) => {
|
|
784
801
|
const f = args.flags;
|
|
785
802
|
const params = {
|
|
786
803
|
metric: f.metric || undefined, category: f.category || undefined,
|
|
787
804
|
days: Number(f.days) || undefined, min_observations: Number(f["min-obs"]) || undefined,
|
|
788
|
-
limit: Number(f.limit) || undefined,
|
|
805
|
+
limit: Number(f.limit) || undefined, language: f.language || undefined,
|
|
806
|
+
...dateFlags(f),
|
|
789
807
|
};
|
|
790
808
|
if (json)
|
|
791
|
-
return printJson(await client.getTopClaims(params));
|
|
809
|
+
return printJson(await client.getTopClaims({ ...params, project: f.collection || undefined }));
|
|
792
810
|
await runHandler("top_claims", params);
|
|
793
811
|
},
|
|
794
812
|
},
|
|
@@ -800,7 +818,7 @@ const COMMANDS = {
|
|
|
800
818
|
run: async (args, json) => {
|
|
801
819
|
const [id] = args.positionals;
|
|
802
820
|
if (!id || !/^\d+$/.test(id)) {
|
|
803
|
-
|
|
821
|
+
printErr("Error: claim_id (numeric) required.");
|
|
804
822
|
process.exit(1);
|
|
805
823
|
}
|
|
806
824
|
const claimId = Number(id);
|
|
@@ -822,15 +840,17 @@ const COMMANDS = {
|
|
|
822
840
|
trend: {
|
|
823
841
|
tool: "detect_trend",
|
|
824
842
|
description: "Detect topic volume trends",
|
|
825
|
-
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_LANGUAGE, FLAG_COUNTRY,
|
|
843
|
+
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_COLLECTION,
|
|
826
844
|
{ name: "recent", type: "number", default: 7, desc: "Recent window in days" },
|
|
827
845
|
{ name: "baseline", type: "number", default: 30, desc: "Baseline window in days" },
|
|
828
846
|
{ name: "days", alias: "d", type: "number", desc: "Alias for --baseline" },
|
|
847
|
+
{ ...FLAG_START, desc: "Baseline start: YYYY, YYYY-MM or YYYY-MM-DD (pins the baseline window)" },
|
|
848
|
+
{ ...FLAG_END, desc: "Baseline end, inclusive (default today); recent = its last --recent days" },
|
|
829
849
|
{ name: "min-similarity", type: "number", desc: "Minimum similarity threshold" }, FLAG_JSON],
|
|
830
850
|
run: async (args, json) => {
|
|
831
851
|
const [topic] = args.positionals;
|
|
832
852
|
if (!topic) {
|
|
833
|
-
|
|
853
|
+
printErr("Error: topic is required.");
|
|
834
854
|
process.exit(1);
|
|
835
855
|
}
|
|
836
856
|
const f = args.flags;
|
|
@@ -843,6 +863,8 @@ const COMMANDS = {
|
|
|
843
863
|
recent_days: Number(f.recent) || 7,
|
|
844
864
|
baseline_days: Number(f.days) || Number(f.baseline) || 30,
|
|
845
865
|
min_similarity: Number(f["min-similarity"]) || undefined,
|
|
866
|
+
project: f.collection || undefined,
|
|
867
|
+
...dateFlags(f),
|
|
846
868
|
};
|
|
847
869
|
if (json)
|
|
848
870
|
return printJson(await client.detectTrend({
|
|
@@ -856,18 +878,28 @@ const COMMANDS = {
|
|
|
856
878
|
tool: "get_timeline",
|
|
857
879
|
description: "Topic mention volume over time",
|
|
858
880
|
specs: [FLAG_DAYS, FLAG_SPEAKER, FLAG_CHANNEL, FLAG_LANGUAGE,
|
|
859
|
-
{ name: "granularity", alias: "g", type: "string", default: "day", desc: "Bucket size: day | week" },
|
|
881
|
+
{ name: "granularity", alias: "g", type: "string", default: "day", desc: "Bucket size: day | week | month | quarter | year (calendar buckets)" },
|
|
860
882
|
{ name: "min-similarity", type: "string", desc: "Relevance threshold 0-1 (default 0.30, same as quote search)" },
|
|
861
|
-
FLAG_START, FLAG_END, FLAG_JSON],
|
|
883
|
+
FLAG_START, FLAG_END, FLAG_COLLECTION, FLAG_JSON],
|
|
862
884
|
run: async (args, json) => {
|
|
863
885
|
const [topic] = args.positionals;
|
|
864
886
|
if (!topic) {
|
|
865
|
-
|
|
887
|
+
printErr("Error: topic is required.");
|
|
866
888
|
process.exit(1);
|
|
867
889
|
}
|
|
868
890
|
const f = args.flags;
|
|
891
|
+
if (!GRANULARITIES.includes(f.granularity)) {
|
|
892
|
+
printErr(`Error: --granularity must be one of ${GRANULARITIES.join(", ")} (got "${f.granularity}").`);
|
|
893
|
+
process.exit(1);
|
|
894
|
+
}
|
|
869
895
|
const days = Number(f.days) || 30;
|
|
870
896
|
const dr = f.start ? { start_date: f.start, end_date: f.end || new Date().toISOString().slice(0, 10) } : daysRange(days);
|
|
897
|
+
if (!f.start && f.end) {
|
|
898
|
+
// --end alone: the --days days ending there, not ending today.
|
|
899
|
+
const end = Date.parse(f.end);
|
|
900
|
+
dr.start_date = new Date(end - days * 86400000).toISOString().slice(0, 10);
|
|
901
|
+
dr.end_date = f.end;
|
|
902
|
+
}
|
|
871
903
|
const tlChannel = f.channel || undefined;
|
|
872
904
|
const tlParams = {
|
|
873
905
|
topic,
|
|
@@ -878,10 +910,62 @@ const COMMANDS = {
|
|
|
878
910
|
language: f.language || undefined,
|
|
879
911
|
granularity: f.granularity || "day",
|
|
880
912
|
min_similarity: f["min-similarity"] != null ? Number(f["min-similarity"]) : undefined,
|
|
913
|
+
project: f.collection || undefined,
|
|
881
914
|
};
|
|
882
915
|
if (json)
|
|
883
916
|
return printJson(await client.getTimeline(tlParams));
|
|
884
|
-
await runHandler("get_timeline", {
|
|
917
|
+
await runHandler("get_timeline", {
|
|
918
|
+
...tlParams, days, channel: tlChannel,
|
|
919
|
+
// Explicit dates only, so the footer states them instead of "last N days".
|
|
920
|
+
start_date: f.start || f.end ? dr.start_date : undefined,
|
|
921
|
+
end_date: f.start || f.end ? dr.end_date : undefined,
|
|
922
|
+
});
|
|
923
|
+
},
|
|
924
|
+
},
|
|
925
|
+
count: {
|
|
926
|
+
tool: "count_mentions",
|
|
927
|
+
description: "Count segments, episodes and channels for a topic — by month, channel, episode, ...",
|
|
928
|
+
specs: [FLAG_DAYS, FLAG_START, FLAG_END, FLAG_COLLECTION, FLAG_SPEAKER, FLAG_CHANNEL,
|
|
929
|
+
{ ...FLAG_LANGUAGE, desc: "Language code(s), comma-separated (default: every language)" },
|
|
930
|
+
FLAG_COUNTRY,
|
|
931
|
+
{ name: "mode", alias: "m", type: "string", default: "semantic", desc: "semantic (estimate) | keyword (exact phrase, exact count)" },
|
|
932
|
+
{ name: "by", long: ["group-by"], alias: "g", type: "string", default: "none", desc: `Break down by: ${COUNT_GROUPS.join(" | ")}` },
|
|
933
|
+
{ name: "vs", type: "string", desc: "More topics to count side by side, comma-separated (up to 5 in all)" },
|
|
934
|
+
{ name: "order", type: "string", default: "segments", desc: "Episode breakdown order: segments | date" },
|
|
935
|
+
{ name: "min-similarity", type: "number", desc: "Semantic threshold (default 0.35)" },
|
|
936
|
+
{ ...FLAG_LIMIT, desc: "Rows for channel/episode/... breakdowns (-n, -k; max 1000)" }, FLAG_OFFSET, FLAG_JSON],
|
|
937
|
+
run: async (args, json) => {
|
|
938
|
+
const f = args.flags;
|
|
939
|
+
const topic = args.positionals.join(" ").trim() || undefined;
|
|
940
|
+
const vs = f.vs?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
941
|
+
if (!COUNT_GROUPS.includes(f.by)) {
|
|
942
|
+
printErr(`Error: --by must be one of ${COUNT_GROUPS.join(", ")} (got "${f.by}").`);
|
|
943
|
+
process.exit(1);
|
|
944
|
+
}
|
|
945
|
+
if (f.mode !== "semantic" && f.mode !== "keyword") {
|
|
946
|
+
printErr(`Error: --mode must be semantic or keyword for count (got "${f.mode}").`);
|
|
947
|
+
process.exit(1);
|
|
948
|
+
}
|
|
949
|
+
const params = {
|
|
950
|
+
topic,
|
|
951
|
+
topics: vs?.length ? [...(topic ? [topic] : []), ...vs] : undefined,
|
|
952
|
+
mode: f.mode,
|
|
953
|
+
group_by: f.by,
|
|
954
|
+
days: f.start || f.end ? undefined : Number(f.days) || 30,
|
|
955
|
+
...dateFlags(f),
|
|
956
|
+
collection: f.collection || undefined,
|
|
957
|
+
speaker: f.speaker || undefined,
|
|
958
|
+
channel: f.channel || undefined,
|
|
959
|
+
language: f.language || undefined,
|
|
960
|
+
country: f.country || undefined,
|
|
961
|
+
min_similarity: f["min-similarity"] != null ? Number(f["min-similarity"]) : undefined,
|
|
962
|
+
limit: Number(f.limit) || 20,
|
|
963
|
+
offset: Number(f.offset) || 0,
|
|
964
|
+
order: f.order,
|
|
965
|
+
};
|
|
966
|
+
if (json)
|
|
967
|
+
params.response_format = "json";
|
|
968
|
+
await runHandler("count_mentions", params);
|
|
885
969
|
},
|
|
886
970
|
},
|
|
887
971
|
speaker: {
|
|
@@ -892,7 +976,7 @@ const COMMANDS = {
|
|
|
892
976
|
run: async (args, json) => {
|
|
893
977
|
const [identity, topic] = args.positionals;
|
|
894
978
|
if (!identity) {
|
|
895
|
-
|
|
979
|
+
printErr("Error: speaker name or ID required.");
|
|
896
980
|
process.exit(1);
|
|
897
981
|
}
|
|
898
982
|
const f = args.flags;
|
|
@@ -965,7 +1049,7 @@ const COMMANDS = {
|
|
|
965
1049
|
run: async (args, json) => {
|
|
966
1050
|
const [id] = args.positionals;
|
|
967
1051
|
if (!id) {
|
|
968
|
-
|
|
1052
|
+
printErr("Error: channel ID or name required.");
|
|
969
1053
|
process.exit(1);
|
|
970
1054
|
}
|
|
971
1055
|
const isId = /^\d+$/.test(id);
|
|
@@ -1000,7 +1084,7 @@ const COMMANDS = {
|
|
|
1000
1084
|
run: async (args, json) => {
|
|
1001
1085
|
const [id] = args.positionals;
|
|
1002
1086
|
if (!id) {
|
|
1003
|
-
|
|
1087
|
+
printErr("Error: content ID required.");
|
|
1004
1088
|
process.exit(1);
|
|
1005
1089
|
}
|
|
1006
1090
|
if (json) {
|
|
@@ -1019,7 +1103,7 @@ const COMMANDS = {
|
|
|
1019
1103
|
run: async (args, json) => {
|
|
1020
1104
|
const [id] = args.positionals;
|
|
1021
1105
|
if (!id) {
|
|
1022
|
-
|
|
1106
|
+
printErr("Error: segment ID required.");
|
|
1023
1107
|
process.exit(1);
|
|
1024
1108
|
}
|
|
1025
1109
|
const segId = parseSegmentId(id);
|
|
@@ -1031,15 +1115,15 @@ const COMMANDS = {
|
|
|
1031
1115
|
thread: {
|
|
1032
1116
|
tool: "get_thread",
|
|
1033
1117
|
description: "Conversation context around a quote",
|
|
1034
|
-
specs: [{ name: "n", type: "number", default: 5, desc: "Segments of context before and after the quote" }, FLAG_JSON],
|
|
1118
|
+
specs: [{ name: "context", alias: "C", long: ["n"], type: "number", default: 5, desc: "Segments of context before and after the quote" }, FLAG_JSON],
|
|
1035
1119
|
run: async (args, json) => {
|
|
1036
1120
|
const [id] = args.positionals;
|
|
1037
1121
|
if (!id) {
|
|
1038
|
-
|
|
1122
|
+
printErr("Error: segment ID required.");
|
|
1039
1123
|
process.exit(1);
|
|
1040
1124
|
}
|
|
1041
1125
|
const segId = parseSegmentId(id);
|
|
1042
|
-
const n = Number(args.flags.
|
|
1126
|
+
const n = Number(args.flags.context) || 5;
|
|
1043
1127
|
if (json)
|
|
1044
1128
|
return printJson(await client.getSegmentThread(segId, n));
|
|
1045
1129
|
await runHandler("get_thread", { segment_id: segId, n });
|
|
@@ -1048,13 +1132,13 @@ const COMMANDS = {
|
|
|
1048
1132
|
analyze: {
|
|
1049
1133
|
tool: "analyze_topic",
|
|
1050
1134
|
description: "AI-powered topic analysis",
|
|
1051
|
-
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_DAYS, FLAG_LANGUAGE, FLAG_COUNTRY,
|
|
1052
|
-
{ name: "workflow",
|
|
1135
|
+
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_DAYS, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_COLLECTION,
|
|
1136
|
+
{ name: "workflow", type: "string", default: "simple_rag", desc: "Workflow: simple_rag | speaker_focus | entity_network | narrative_timeline | hierarchical_summary | discourse_summary" },
|
|
1053
1137
|
FLAG_SKIP_CACHE, FLAG_OUT, FLAG_JSON],
|
|
1054
1138
|
run: async (args, json) => {
|
|
1055
1139
|
const [query] = args.positionals;
|
|
1056
1140
|
if (!query) {
|
|
1057
|
-
|
|
1141
|
+
printErr("Error: query is required.");
|
|
1058
1142
|
process.exit(1);
|
|
1059
1143
|
}
|
|
1060
1144
|
const f = args.flags;
|
|
@@ -1068,6 +1152,7 @@ const COMMANDS = {
|
|
|
1068
1152
|
country: f.country || undefined,
|
|
1069
1153
|
days: Number(f.days) || undefined,
|
|
1070
1154
|
skip_cache: f["skip-cache"],
|
|
1155
|
+
project: f.collection || undefined,
|
|
1071
1156
|
};
|
|
1072
1157
|
if (json)
|
|
1073
1158
|
return printJson(await client.analyze(params));
|
|
@@ -1077,11 +1162,11 @@ const COMMANDS = {
|
|
|
1077
1162
|
frames: {
|
|
1078
1163
|
tool: "compare_frames",
|
|
1079
1164
|
description: "Compare left vs right media framing on a topic",
|
|
1080
|
-
specs: [FLAG_DAYS, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_SKIP_CACHE, FLAG_OUT, FLAG_JSON],
|
|
1165
|
+
specs: [FLAG_DAYS, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_COLLECTION, FLAG_SKIP_CACHE, FLAG_OUT, FLAG_JSON],
|
|
1081
1166
|
run: async (args, json) => {
|
|
1082
1167
|
const [topic] = args.positionals;
|
|
1083
1168
|
if (!topic) {
|
|
1084
|
-
|
|
1169
|
+
printErr("Error: topic is required.");
|
|
1085
1170
|
process.exit(1);
|
|
1086
1171
|
}
|
|
1087
1172
|
const f = args.flags;
|
|
@@ -1093,7 +1178,7 @@ const COMMANDS = {
|
|
|
1093
1178
|
skip_cache: f["skip-cache"],
|
|
1094
1179
|
};
|
|
1095
1180
|
if (json) {
|
|
1096
|
-
const shared = { query: topic, workflow: "deep_analysis", days: params.days, language: params.language, country: params.country, skip_cache: params.skip_cache };
|
|
1181
|
+
const shared = { query: topic, workflow: "deep_analysis", days: params.days, language: params.language, country: params.country, skip_cache: params.skip_cache, project: f.collection || undefined };
|
|
1097
1182
|
const [left, right] = await Promise.all([
|
|
1098
1183
|
client.analyze({ ...shared, political_lean_min: 0.0, political_lean_max: 0.35 }),
|
|
1099
1184
|
client.analyze({ ...shared, political_lean_min: 0.65, political_lean_max: 1.0 }),
|
|
@@ -1108,18 +1193,18 @@ const COMMANDS = {
|
|
|
1108
1193
|
description: "Compare speakers on a topic",
|
|
1109
1194
|
// FLAG_DAYS carries default:30, which silently overrode this command's
|
|
1110
1195
|
// intended 90d window — spread it with the right default.
|
|
1111
|
-
specs: [{ name: "speakers", type: "string", desc: "2-5 comma-separated speaker names" }, FLAG_SPEAKER, { ...FLAG_DAYS, default: 90 }, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_SKIP_CACHE, FLAG_OUT, FLAG_JSON],
|
|
1196
|
+
specs: [{ name: "speakers", type: "string", desc: "2-5 comma-separated speaker names" }, FLAG_SPEAKER, { ...FLAG_DAYS, default: 90 }, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_COLLECTION, FLAG_SKIP_CACHE, FLAG_OUT, FLAG_JSON],
|
|
1112
1197
|
run: async (args, json) => {
|
|
1113
1198
|
const [topic] = args.positionals;
|
|
1114
1199
|
if (!topic) {
|
|
1115
|
-
|
|
1200
|
+
printErr("Error: topic is required.");
|
|
1116
1201
|
process.exit(1);
|
|
1117
1202
|
}
|
|
1118
1203
|
const f = args.flags;
|
|
1119
1204
|
const speakersStr = f.speakers || f.speaker || "";
|
|
1120
1205
|
const speakers = speakersStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1121
1206
|
if (speakers.length < 2) {
|
|
1122
|
-
|
|
1207
|
+
printErr("Error: --speakers requires 2-5 comma-separated names.");
|
|
1123
1208
|
process.exit(1);
|
|
1124
1209
|
}
|
|
1125
1210
|
process.stderr.write("Comparing speakers... (may take 1-3 minutes)\n");
|
|
@@ -1133,7 +1218,7 @@ const COMMANDS = {
|
|
|
1133
1218
|
return printJson(await client.analyze({
|
|
1134
1219
|
query: topic, workflow: "speaker_compare", speaker_names: speakers,
|
|
1135
1220
|
language: params.language, country: params.country,
|
|
1136
|
-
days: params.days, skip_cache: params.skip_cache,
|
|
1221
|
+
days: params.days, skip_cache: params.skip_cache, project: f.collection || undefined,
|
|
1137
1222
|
}));
|
|
1138
1223
|
await runHandler("compare_speakers", params, f.out);
|
|
1139
1224
|
},
|
|
@@ -1150,14 +1235,14 @@ const COMMANDS = {
|
|
|
1150
1235
|
run: async (args, json) => {
|
|
1151
1236
|
const [topic] = args.positionals;
|
|
1152
1237
|
if (!topic) {
|
|
1153
|
-
|
|
1238
|
+
printErr("Error: topic is required.");
|
|
1154
1239
|
process.exit(1);
|
|
1155
1240
|
}
|
|
1156
1241
|
if (args.flags.speakers)
|
|
1157
1242
|
return runDossierBatch(topic, args, json);
|
|
1158
1243
|
const speaker = args.flags.speaker;
|
|
1159
1244
|
if (!speaker) {
|
|
1160
|
-
|
|
1245
|
+
printErr("Error: --speaker is required (or --speakers <file> for batch mode).");
|
|
1161
1246
|
process.exit(1);
|
|
1162
1247
|
}
|
|
1163
1248
|
process.stderr.write("Building dossier... (may take 1-3 minutes)\n");
|
|
@@ -1178,18 +1263,18 @@ const COMMANDS = {
|
|
|
1178
1263
|
run: async (args, json) => {
|
|
1179
1264
|
const [topic] = args.positionals;
|
|
1180
1265
|
if (!topic) {
|
|
1181
|
-
|
|
1266
|
+
printErr("Error: topic is required.");
|
|
1182
1267
|
process.exit(1);
|
|
1183
1268
|
}
|
|
1184
1269
|
const f = args.flags;
|
|
1185
1270
|
const speaker = f.speaker;
|
|
1186
1271
|
if (!speaker) {
|
|
1187
|
-
|
|
1272
|
+
printErr("Error: --speaker is required.");
|
|
1188
1273
|
process.exit(1);
|
|
1189
1274
|
}
|
|
1190
1275
|
const periods = (f.periods || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1191
1276
|
if (periods.length < 2) {
|
|
1192
|
-
|
|
1277
|
+
printErr("Error: --periods requires 2+ comma-separated periods (e.g. 2024,2026).");
|
|
1193
1278
|
process.exit(1);
|
|
1194
1279
|
}
|
|
1195
1280
|
process.stderr.write("Comparing periods... (may take 1-3 minutes)\n");
|
|
@@ -1200,7 +1285,7 @@ const COMMANDS = {
|
|
|
1200
1285
|
if (json)
|
|
1201
1286
|
return printJson(data);
|
|
1202
1287
|
if (data.error) {
|
|
1203
|
-
|
|
1288
|
+
printErr(`Error: ${data.error}`);
|
|
1204
1289
|
process.exit(1);
|
|
1205
1290
|
}
|
|
1206
1291
|
const lines = [`**Speaker evolution: ${speaker} on ${topic} (${periods.join(" vs ")})**\n`];
|
|
@@ -1233,12 +1318,12 @@ const COMMANDS = {
|
|
|
1233
1318
|
run: async (args, json) => {
|
|
1234
1319
|
const [topic] = args.positionals;
|
|
1235
1320
|
if (!topic) {
|
|
1236
|
-
|
|
1321
|
+
printErr("Error: topic is required.");
|
|
1237
1322
|
process.exit(1);
|
|
1238
1323
|
}
|
|
1239
1324
|
const speaker = args.flags.speaker;
|
|
1240
1325
|
if (!speaker) {
|
|
1241
|
-
|
|
1326
|
+
printErr("Error: --speaker is required.");
|
|
1242
1327
|
process.exit(1);
|
|
1243
1328
|
}
|
|
1244
1329
|
process.stderr.write("Checking contradictions... (may take 1-3 minutes)\n");
|
|
@@ -1253,22 +1338,32 @@ const COMMANDS = {
|
|
|
1253
1338
|
briefing: {
|
|
1254
1339
|
tool: "get_briefing",
|
|
1255
1340
|
description: "AI discourse briefing",
|
|
1256
|
-
specs: [{ ...FLAG_DAYS, default: 3, desc: "days to cover (1-14)" }, { name: "max-topics", type: "number", default: 8, desc: "Max topics in the briefing" }, FLAG_JSON],
|
|
1341
|
+
specs: [{ ...FLAG_DAYS, default: 3, desc: "days to cover (1-14)" }, { name: "max-topics", type: "number", default: 8, desc: "Max topics in the briefing" }, FLAG_COLLECTION, FLAG_JSON],
|
|
1257
1342
|
run: async (args, json) => {
|
|
1258
1343
|
const f = args.flags;
|
|
1259
1344
|
const params = { days: Number(f.days) || 3, max_topics: Number(f["max-topics"]) || 8 };
|
|
1260
1345
|
if (json)
|
|
1261
|
-
return printJson(await client.getDiscourseSummary(params));
|
|
1346
|
+
return printJson(await client.getDiscourseSummary({ ...params, project: f.collection || undefined }));
|
|
1262
1347
|
await runHandler("get_briefing", params);
|
|
1263
1348
|
},
|
|
1264
1349
|
},
|
|
1350
|
+
collections: {
|
|
1351
|
+
tool: "list_collections",
|
|
1352
|
+
description: "List searchable collections (use with --collection)",
|
|
1353
|
+
specs: [FLAG_JSON],
|
|
1354
|
+
run: async (_args, json) => {
|
|
1355
|
+
if (json)
|
|
1356
|
+
return printJson(await client.getCollections());
|
|
1357
|
+
await runHandler("list_collections", {});
|
|
1358
|
+
},
|
|
1359
|
+
},
|
|
1265
1360
|
stats: {
|
|
1266
1361
|
tool: "get_platform_stats",
|
|
1267
1362
|
description: "Platform statistics",
|
|
1268
|
-
specs: [FLAG_DAYS, { name: "projects", type: "boolean", desc: "Per-
|
|
1363
|
+
specs: [FLAG_DAYS, { name: "collections", long: ["projects"], type: "boolean", desc: "Per-collection stats instead of platform totals" }, FLAG_JSON],
|
|
1269
1364
|
run: async (args, json) => {
|
|
1270
1365
|
const f = args.flags;
|
|
1271
|
-
if (f.
|
|
1366
|
+
if (f.collections) {
|
|
1272
1367
|
if (json)
|
|
1273
1368
|
return printJson(await client.getProjectStats());
|
|
1274
1369
|
await runHandler("get_project_stats", {});
|
|
@@ -1284,23 +1379,23 @@ const COMMANDS = {
|
|
|
1284
1379
|
"top-channels": {
|
|
1285
1380
|
tool: "get_top_channels",
|
|
1286
1381
|
description: "Top channels by episode count",
|
|
1287
|
-
specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_JSON],
|
|
1382
|
+
specs: [FLAG_DAYS, FLAG_START, FLAG_END, FLAG_LIMIT, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_COLLECTION, FLAG_JSON],
|
|
1288
1383
|
run: async (args, json) => {
|
|
1289
1384
|
const f = args.flags;
|
|
1290
|
-
const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 20, language: f.language || undefined, country: f.country || undefined };
|
|
1385
|
+
const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 20, language: f.language || undefined, country: f.country || undefined, ...dateFlags(f) };
|
|
1291
1386
|
if (json)
|
|
1292
|
-
return printJson(await client.getTopChannels(params));
|
|
1387
|
+
return printJson(await client.getTopChannels({ ...params, project: f.collection || undefined }));
|
|
1293
1388
|
await runHandler("get_top_channels", params);
|
|
1294
1389
|
},
|
|
1295
1390
|
},
|
|
1296
1391
|
"top-speakers": {
|
|
1297
1392
|
tool: "get_top_speakers",
|
|
1298
1393
|
description: "Top speakers by appearances, talk time, or clips",
|
|
1299
|
-
specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_LANGUAGE, FLAG_COUNTRY,
|
|
1300
|
-
{ name: "ranking",
|
|
1394
|
+
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" }, FLAG_JSON],
|
|
1301
1396
|
run: async (args, json) => {
|
|
1302
1397
|
const f = args.flags;
|
|
1303
|
-
const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 20, ranking: f.ranking, language: f.language || undefined, country: f.country || undefined };
|
|
1398
|
+
const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 20, ranking: f.ranking, language: f.language || undefined, country: f.country || undefined, project: f.collection || undefined, ...dateFlags(f) };
|
|
1304
1399
|
if (json)
|
|
1305
1400
|
return printJson(await client.getTopSpeakers(params));
|
|
1306
1401
|
await runHandler("get_top_speakers", params);
|
|
@@ -1309,10 +1404,10 @@ const COMMANDS = {
|
|
|
1309
1404
|
network: {
|
|
1310
1405
|
tool: "get_speaker_network",
|
|
1311
1406
|
description: "Speaker co-appearance network",
|
|
1312
|
-
specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_LANGUAGE, FLAG_JSON],
|
|
1407
|
+
specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_LANGUAGE, FLAG_COLLECTION, FLAG_JSON],
|
|
1313
1408
|
run: async (args, json) => {
|
|
1314
1409
|
const f = args.flags;
|
|
1315
|
-
const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 20, language: f.language || "en" };
|
|
1410
|
+
const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 20, language: f.language || "en", project: f.collection || undefined };
|
|
1316
1411
|
if (json)
|
|
1317
1412
|
return printJson(await client.getSpeakerNetwork(params));
|
|
1318
1413
|
await runHandler("get_speaker_network", params);
|
|
@@ -1322,7 +1417,7 @@ const COMMANDS = {
|
|
|
1322
1417
|
tool: "get_topics",
|
|
1323
1418
|
description: "Trending topic clusters",
|
|
1324
1419
|
specs: [FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_LIMIT, FLAG_DAYS,
|
|
1325
|
-
{ name: "query",
|
|
1420
|
+
{ name: "query", type: "string", desc: "Semantic filter — only clusters relevant to this query" },
|
|
1326
1421
|
{ name: "min-segments", type: "number", default: 50, desc: "Minimum segments per cluster" },
|
|
1327
1422
|
FLAG_START, FLAG_END, FLAG_JSON],
|
|
1328
1423
|
run: async (args, json) => {
|
|
@@ -1381,7 +1476,7 @@ const COMMANDS = {
|
|
|
1381
1476
|
const profile = await client.getChannelByName(channelArg);
|
|
1382
1477
|
const resolvedId = profile.channel_id ?? profile.id;
|
|
1383
1478
|
if (!resolvedId) {
|
|
1384
|
-
|
|
1479
|
+
printErr(`Error: channel "${channelArg}" not found.`);
|
|
1385
1480
|
process.exit(1);
|
|
1386
1481
|
}
|
|
1387
1482
|
if (json)
|
|
@@ -1451,7 +1546,7 @@ async function cmdSetup(force) {
|
|
|
1451
1546
|
const key = await rl.question("API key: ");
|
|
1452
1547
|
rl.close();
|
|
1453
1548
|
if (!key.trim()) {
|
|
1454
|
-
|
|
1549
|
+
printErr("Error: no API key provided.");
|
|
1455
1550
|
process.exit(1);
|
|
1456
1551
|
}
|
|
1457
1552
|
process.env.SIGNAL4_API_KEY = key.trim();
|
|
@@ -1460,7 +1555,7 @@ async function cmdSetup(force) {
|
|
|
1460
1555
|
print(`Authenticated as ${who.user_email ?? who.key_prefix ?? "unknown"}`);
|
|
1461
1556
|
}
|
|
1462
1557
|
catch (err) {
|
|
1463
|
-
|
|
1558
|
+
printErr(`Error: invalid API key (${err instanceof Error ? err.message : String(err)})`);
|
|
1464
1559
|
process.exit(1);
|
|
1465
1560
|
}
|
|
1466
1561
|
if (!existsSync(configDir))
|
|
@@ -1552,7 +1647,7 @@ async function showHelp() {
|
|
|
1552
1647
|
lines.push(`\u2500\u2500 ${tier} \u2500\u2500`);
|
|
1553
1648
|
for (const c of cmds) {
|
|
1554
1649
|
shown.add(c.name);
|
|
1555
|
-
lines.push(` ${c.name.padEnd(19)}${c.desc}`);
|
|
1650
|
+
lines.push(` ${c.name.padEnd(19)}${c.desc}${speedWord(c.name)}`);
|
|
1556
1651
|
}
|
|
1557
1652
|
lines.push("");
|
|
1558
1653
|
}
|
|
@@ -1563,7 +1658,7 @@ async function showHelp() {
|
|
|
1563
1658
|
lines.push(`\u2500\u2500 ${tier} \u2500\u2500`);
|
|
1564
1659
|
for (const c of cmds) {
|
|
1565
1660
|
shown.add(c.name);
|
|
1566
|
-
lines.push(` ${c.name.padEnd(19)}${c.desc}`);
|
|
1661
|
+
lines.push(` ${c.name.padEnd(19)}${c.desc}${speedWord(c.name)}`);
|
|
1567
1662
|
}
|
|
1568
1663
|
lines.push("");
|
|
1569
1664
|
}
|
|
@@ -1576,7 +1671,7 @@ async function showHelp() {
|
|
|
1576
1671
|
lines.push("\u2500\u2500 also available \u2500\u2500");
|
|
1577
1672
|
for (const n of missing) {
|
|
1578
1673
|
const pad = n.length >= 19 ? `${n} ` : n.padEnd(19);
|
|
1579
|
-
lines.push(` ${pad}${COMMANDS[n].description}`);
|
|
1674
|
+
lines.push(` ${pad}${COMMANDS[n].description}${speedWord(n)}`);
|
|
1580
1675
|
}
|
|
1581
1676
|
lines.push("");
|
|
1582
1677
|
}
|
|
@@ -1595,11 +1690,14 @@ async function showHelp() {
|
|
|
1595
1690
|
lines.push(` ${data.available_commands.join(", ")}`);
|
|
1596
1691
|
lines.push("");
|
|
1597
1692
|
}
|
|
1693
|
+
lines.push("Speed: instant <0.5s · fast 0.5–2.5s · slow >2.5s (at default settings; longer windows take longer — see s4 <command> --help). Slow commands print an estimate to stderr first.");
|
|
1694
|
+
lines.push("");
|
|
1598
1695
|
lines.push("Flags:");
|
|
1599
1696
|
lines.push(" --json Raw JSON output");
|
|
1600
1697
|
lines.push(" --help, -h Show this help");
|
|
1601
1698
|
lines.push(" s4 <command> --help Flags for one command (flags vary by command)");
|
|
1602
|
-
lines.push(" --
|
|
1699
|
+
lines.push(" --collection NAME Scope to one collection (s4 collections); omit = whole corpus");
|
|
1700
|
+
lines.push(" -V, --version Version and build time");
|
|
1603
1701
|
lines.push("");
|
|
1604
1702
|
lines.push("MCP server:");
|
|
1605
1703
|
lines.push(" s4 --mcp Start MCP server for Claude Desktop / Cursor");
|
|
@@ -1614,10 +1712,17 @@ async function showHelp() {
|
|
|
1614
1712
|
].join("\n"));
|
|
1615
1713
|
}
|
|
1616
1714
|
}
|
|
1715
|
+
/** " (fast)" after a command in the global help, from speed.ts; nothing for unknown commands. */
|
|
1716
|
+
function speedWord(name) {
|
|
1717
|
+
const cmd = COMMANDS[name];
|
|
1718
|
+
return cmd ? ` (${estimate(cmd.tool, {}).tier})` : "";
|
|
1719
|
+
}
|
|
1617
1720
|
function showCommandHelp(name, cmd) {
|
|
1618
1721
|
const lines = [
|
|
1619
1722
|
`s4 ${name} — ${cmd.description}`,
|
|
1620
1723
|
"",
|
|
1724
|
+
`Speed: ${descriptionTag(cmd.tool).slice(1, -1)} (instant <0.5s · fast 0.5–2.5s · slow >2.5s)`,
|
|
1725
|
+
"",
|
|
1621
1726
|
`Usage: s4 ${name} [args] [flags]`,
|
|
1622
1727
|
"",
|
|
1623
1728
|
"Flags:",
|
|
@@ -1634,12 +1739,35 @@ function showCommandHelp(name, cmd) {
|
|
|
1634
1739
|
// ---------------------------------------------------------------------------
|
|
1635
1740
|
// Main
|
|
1636
1741
|
// ---------------------------------------------------------------------------
|
|
1742
|
+
const COMMANDS_WITH_TOPIC = new Set([
|
|
1743
|
+
"search", "quotes", "claims", "analyze-claims", "trend-claims", "contradict-claims", "compare-claims",
|
|
1744
|
+
"channel-dossier", "compare-channels", "channel-contradictions", "trend", "timeline", "analyze",
|
|
1745
|
+
"frames", "compare", "dossier", "contradictions", "compare-periods", "entities",
|
|
1746
|
+
]);
|
|
1747
|
+
/** Commands whose backend takes explicit dates, so --days has no ceiling. */
|
|
1748
|
+
const DATE_RANGE_COMMANDS = new Set(["search", "quotes", "claims", "timeline", "count"]);
|
|
1749
|
+
const BUILD_INFO = (() => {
|
|
1750
|
+
try {
|
|
1751
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
1752
|
+
const built = statSync(new URL("./cli.js", import.meta.url)).mtime.toISOString().slice(0, 16).replace("T", " ");
|
|
1753
|
+
return `s4 ${pkg.version} (built ${built} UTC)`;
|
|
1754
|
+
}
|
|
1755
|
+
catch {
|
|
1756
|
+
return "s4 (version unknown)";
|
|
1757
|
+
}
|
|
1758
|
+
})();
|
|
1759
|
+
const BUILTIN_COMMANDS = ["setup", "whoami", "version", "mcp", "help"];
|
|
1637
1760
|
export async function main() {
|
|
1638
1761
|
applyConfig();
|
|
1762
|
+
setSurface("cli");
|
|
1639
1763
|
const argv = process.argv.slice(2);
|
|
1764
|
+
if (argv[0] === "version" || argv[0] === "-V" || argv[0] === "--version") {
|
|
1765
|
+
print(BUILD_INFO);
|
|
1766
|
+
process.exit(0);
|
|
1767
|
+
}
|
|
1640
1768
|
// Global help — but `s4 <command> --help` gets that command's flags instead.
|
|
1641
1769
|
const wantsHelp = argv.includes("--help") || argv.includes("-h");
|
|
1642
|
-
if (!argv.length || (wantsHelp && !COMMANDS[argv[0]])) {
|
|
1770
|
+
if (!argv.length || argv[0] === "help" || (wantsHelp && !COMMANDS[argv[0]])) {
|
|
1643
1771
|
await showHelp();
|
|
1644
1772
|
process.exit(0);
|
|
1645
1773
|
}
|
|
@@ -1652,6 +1780,8 @@ export async function main() {
|
|
|
1652
1780
|
await import("./index.js");
|
|
1653
1781
|
return;
|
|
1654
1782
|
}
|
|
1783
|
+
// CLI mode gets its own UA (MCP entry points keep the default).
|
|
1784
|
+
client.setUserAgent("Signal4-S4CLI/2.7");
|
|
1655
1785
|
const command = argv[0];
|
|
1656
1786
|
const rest = argv.slice(1);
|
|
1657
1787
|
// Setup (no API key needed)
|
|
@@ -1672,12 +1802,24 @@ export async function main() {
|
|
|
1672
1802
|
// Lookup command
|
|
1673
1803
|
const cmd = COMMANDS[command];
|
|
1674
1804
|
if (!cmd) {
|
|
1675
|
-
|
|
1805
|
+
const near = didYouMean(command, [...Object.keys(COMMANDS), ...BUILTIN_COMMANDS]);
|
|
1806
|
+
process.stderr.write(`Error: unknown command "${command}"${near ? ` — did you mean "${near}"?` : ""}\nRun s4 --help for usage.\n`);
|
|
1676
1807
|
process.exit(1);
|
|
1677
1808
|
}
|
|
1678
1809
|
// Parse args
|
|
1679
1810
|
const args = parseArgs(rest, cmd.specs);
|
|
1811
|
+
// Unquoted multi-word topics: `s4 search immigration policy`.
|
|
1812
|
+
if (COMMANDS_WITH_TOPIC.has(command) && args.positionals.length > 1) {
|
|
1813
|
+
args.positionals = [args.positionals.join(" ")];
|
|
1814
|
+
}
|
|
1680
1815
|
const json = args.flags.json;
|
|
1816
|
+
activeCollection = args.flags.collection || undefined;
|
|
1817
|
+
// --days has no ceiling where the backend takes dates; report endpoints cap at 365.
|
|
1818
|
+
const days = args.flags.days;
|
|
1819
|
+
if (typeof days === "number" && days > 365 && !args.flags.start && !DATE_RANGE_COMMANDS.has(command)) {
|
|
1820
|
+
process.stderr.write(`Error: --days is capped at 365 for "${command}". For older material use s4 search / claims / timeline (any --days, or --since 2023).\n`);
|
|
1821
|
+
process.exit(1);
|
|
1822
|
+
}
|
|
1681
1823
|
// Narrate pipeline steps on stderr while analytical commands stream.
|
|
1682
1824
|
let progress = null;
|
|
1683
1825
|
if (PROGRESS_COMMANDS.has(command)) {
|