@signal4/s4 2.6.1 → 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 -176
- 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 +77 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +123 -22
- package/dist/client.js.map +1 -1
- package/dist/format.d.ts +76 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +197 -44
- 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 +60 -24
- 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 +41 -12
- package/dist/handlers/reports.js.map +1 -1
- package/dist/handlers/search.d.ts.map +1 -1
- package/dist/handlers/search.js +67 -21
- 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 +151 -8
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1,106 +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" };
|
|
96
|
-
const
|
|
36
|
+
const FLAG_COLLECTION = { name: "collection", long: ["project"], type: "string", desc: "Restrict to one collection (see: s4 collections). Default: whole corpus" };
|
|
97
37
|
const FLAG_MODE = { name: "mode", alias: "m", type: "string", default: "semantic", desc: "Search mode: semantic | keyword | hybrid" };
|
|
98
|
-
const FLAG_START = { name: "start", type: "string", desc: "Start date YYYY-MM-DD (
|
|
99
|
-
const FLAG_END = { name: "end", type: "string", desc: "End date YYYY-MM-DD (default today)" };
|
|
100
|
-
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)" };
|
|
101
41
|
const FLAG_SKIP_CACHE = { name: "skip-cache", type: "boolean", desc: "Force fresh analysis (bypass cache)" };
|
|
102
|
-
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)" };
|
|
103
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
|
+
}
|
|
104
55
|
// ---------------------------------------------------------------------------
|
|
105
56
|
// Progress reporting for long-running analysis commands
|
|
106
57
|
// ---------------------------------------------------------------------------
|
|
@@ -211,11 +162,15 @@ function parseSegmentId(raw) {
|
|
|
211
162
|
// Accept all of: N, seg:N, seg_N, {seg_N}, {seg:N}.
|
|
212
163
|
const id = Number(raw.replace(/^\{(.*)\}$/, "$1").replace(/^seg[:_]/, ""));
|
|
213
164
|
if (!Number.isInteger(id)) {
|
|
214
|
-
|
|
165
|
+
printErr(`Error: invalid segment ID "${raw}" (expected a number, seg:<number>, or seg_<number>).`);
|
|
215
166
|
process.exit(1);
|
|
216
167
|
}
|
|
217
168
|
return id;
|
|
218
169
|
}
|
|
170
|
+
function printErr(text) {
|
|
171
|
+
activeProgress?.finish();
|
|
172
|
+
process.stderr.write(text + "\n");
|
|
173
|
+
}
|
|
219
174
|
function print(text) {
|
|
220
175
|
activeProgress?.finish();
|
|
221
176
|
process.stdout.write(text + "\n");
|
|
@@ -239,13 +194,25 @@ function toCsv(rows, columns) {
|
|
|
239
194
|
function printJson(data) {
|
|
240
195
|
print(JSON.stringify(data, null, 2));
|
|
241
196
|
}
|
|
197
|
+
/** The parsed --collection for the running command; injected into every handler call. */
|
|
198
|
+
let activeCollection;
|
|
242
199
|
async function runHandler(tool, params, out) {
|
|
243
200
|
const handler = HANDLERS[tool];
|
|
244
201
|
if (!handler) {
|
|
245
|
-
|
|
202
|
+
printErr(`Error: handler "${tool}" not found.`);
|
|
246
203
|
process.exit(1);
|
|
247
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();
|
|
248
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`);
|
|
249
216
|
if (result.isError) {
|
|
250
217
|
process.stderr.write((result.content[0]?.text ?? "Unknown error") + "\n");
|
|
251
218
|
process.exit(1);
|
|
@@ -256,6 +223,41 @@ async function runHandler(tool, params, out) {
|
|
|
256
223
|
else
|
|
257
224
|
print(text);
|
|
258
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
|
+
}
|
|
259
261
|
// ---------------------------------------------------------------------------
|
|
260
262
|
// Batch dossiers (surrogate-roster sweeps): s4 dossier "<topic>" --speakers file.txt --out dir/
|
|
261
263
|
// ---------------------------------------------------------------------------
|
|
@@ -409,21 +411,22 @@ const COMMANDS = {
|
|
|
409
411
|
search: {
|
|
410
412
|
tool: "search_quotes",
|
|
411
413
|
description: "Search quotes by topic and/or speaker",
|
|
412
|
-
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,
|
|
413
415
|
{ name: "sort", type: "string", default: "relevance", desc: "relevance | chronological" },
|
|
414
416
|
{ name: "count", type: "boolean", desc: "Return only the match count" },
|
|
415
417
|
{ name: "full-history", type: "boolean", desc: "Search the entire corpus (requires topic + speaker)" },
|
|
416
418
|
{ name: "min-similarity", type: "number", desc: "Minimum similarity threshold (0.0-1.0, default 0.40)" },
|
|
417
419
|
FLAG_KEYWORD,
|
|
418
420
|
{ name: "speaker-id", type: "number", desc: "Speaker identity ID (from s4 speaker)" }, FLAG_OUT,
|
|
419
|
-
FLAG_START, FLAG_END,
|
|
421
|
+
FLAG_START, FLAG_END, FLAG_COLLECTION,
|
|
420
422
|
FLAG_JSON],
|
|
421
423
|
run: async (args, json) => {
|
|
422
424
|
const [positionalTopic] = args.positionals;
|
|
423
425
|
const f = args.flags;
|
|
424
426
|
const keyword = f.keyword || undefined;
|
|
425
|
-
const topic =
|
|
427
|
+
const topic = keyword || positionalTopic || undefined;
|
|
426
428
|
const mode = keyword ? "keyword" : f.mode;
|
|
429
|
+
await assertChannelsExistForKeywordMode(f.channel || undefined, mode);
|
|
427
430
|
const days = Number(f.days) || 30;
|
|
428
431
|
const fullHistory = f["full-history"];
|
|
429
432
|
// Full history means FULL history — only pass dates given explicitly,
|
|
@@ -442,6 +445,7 @@ const COMMANDS = {
|
|
|
442
445
|
count_only: f.count, sort: f.sort,
|
|
443
446
|
full_history: fullHistory,
|
|
444
447
|
min_similarity: f["min-similarity"] !== undefined ? Number(f["min-similarity"]) : undefined,
|
|
448
|
+
project: f.collection || undefined,
|
|
445
449
|
};
|
|
446
450
|
const out = f.out;
|
|
447
451
|
if (json || (out && out.endsWith(".csv"))) {
|
|
@@ -456,6 +460,7 @@ const COMMANDS = {
|
|
|
456
460
|
full_history: fullHistory || undefined,
|
|
457
461
|
count_only: params.count_only || undefined,
|
|
458
462
|
min_similarity: params.min_similarity,
|
|
463
|
+
project: params.project,
|
|
459
464
|
});
|
|
460
465
|
if (out && out.endsWith(".csv")) {
|
|
461
466
|
const rows = (data.quotes ?? []).map((q) => ({
|
|
@@ -485,7 +490,7 @@ const COMMANDS = {
|
|
|
485
490
|
claims: {
|
|
486
491
|
tool: "search_claims",
|
|
487
492
|
description: "Search extracted claims",
|
|
488
|
-
specs: [{ name: "entity",
|
|
493
|
+
specs: [{ name: "entity", type: "string", desc: "Filter by entity name (person, org, place, subject)" }, FLAG_SPEAKER, FLAG_CHANNEL,
|
|
489
494
|
{ name: "category", type: "string", desc: "Topic category filter" },
|
|
490
495
|
{ name: "stance", type: "string", desc: "Filter by stance: assert | deny | speculate | attribute" },
|
|
491
496
|
FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_MODE, FLAG_KEYWORD,
|
|
@@ -495,12 +500,12 @@ const COMMANDS = {
|
|
|
495
500
|
{ name: "political-lean-min", type: "number", desc: "Min channel political lean (0=left, 1=right)" },
|
|
496
501
|
{ name: "political-lean-max", type: "number", desc: "Max channel political lean (0=left, 1=right)" },
|
|
497
502
|
{ name: "source", type: "string", desc: "Claim source filter" },
|
|
498
|
-
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],
|
|
499
504
|
run: async (args, json) => {
|
|
500
505
|
const [positionalTopic] = args.positionals;
|
|
501
506
|
const f = args.flags;
|
|
502
507
|
const keyword = f.keyword || undefined;
|
|
503
|
-
const topic =
|
|
508
|
+
const topic = keyword || positionalTopic || undefined;
|
|
504
509
|
const mode = keyword ? "keyword" : f.mode;
|
|
505
510
|
const days = Number(f.days) || 30;
|
|
506
511
|
const dr = f.start ? { start_date: f.start, end_date: f.end || new Date().toISOString().slice(0, 10) } : undefined;
|
|
@@ -521,6 +526,7 @@ const COMMANDS = {
|
|
|
521
526
|
days, mode,
|
|
522
527
|
sort: f.sort, min_observations: Number(f["min-obs"]) || 1,
|
|
523
528
|
limit: Number(f.limit), offset: Number(f.offset),
|
|
529
|
+
project: f.collection || undefined,
|
|
524
530
|
...dr,
|
|
525
531
|
};
|
|
526
532
|
const claimsDr = dr ?? daysRange(days);
|
|
@@ -536,6 +542,7 @@ const COMMANDS = {
|
|
|
536
542
|
mode: params.mode, sort: params.sort,
|
|
537
543
|
min_observations: params.min_observations > 1 ? params.min_observations : undefined,
|
|
538
544
|
limit: params.limit, offset: params.offset,
|
|
545
|
+
project: params.project,
|
|
539
546
|
}));
|
|
540
547
|
await runHandler("search_claims", params, f.out);
|
|
541
548
|
},
|
|
@@ -543,12 +550,12 @@ const COMMANDS = {
|
|
|
543
550
|
"analyze-claims": {
|
|
544
551
|
tool: "analyze_claims",
|
|
545
552
|
description: "Analyze a topic over extracted claims — themes with observation stats (fast, no LLM)",
|
|
546
|
-
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,
|
|
547
554
|
{ name: "min-obs", type: "number", default: 1, desc: "Minimum observation count per claim" }, FLAG_JSON],
|
|
548
555
|
run: async (args, json) => {
|
|
549
556
|
const [topic] = args.positionals;
|
|
550
557
|
if (!topic) {
|
|
551
|
-
|
|
558
|
+
printErr("Error: topic is required.");
|
|
552
559
|
process.exit(1);
|
|
553
560
|
}
|
|
554
561
|
const f = args.flags;
|
|
@@ -560,11 +567,13 @@ const COMMANDS = {
|
|
|
560
567
|
language: f.language || undefined,
|
|
561
568
|
days: Number(f.days) || undefined,
|
|
562
569
|
min_observations: Number(f["min-obs"]) || undefined,
|
|
570
|
+
...dateFlags(f),
|
|
563
571
|
};
|
|
564
572
|
if (json)
|
|
565
573
|
return printJson(await client.analyzeTopicClaims({
|
|
566
574
|
topic, speaker_name: params.speaker, channel_names: params.channels,
|
|
567
575
|
language: params.language, days: params.days, min_observations: params.min_observations,
|
|
576
|
+
project: f.collection || undefined, ...dateFlags(f),
|
|
568
577
|
}));
|
|
569
578
|
await runHandler("analyze_claims", params);
|
|
570
579
|
},
|
|
@@ -574,11 +583,12 @@ const COMMANDS = {
|
|
|
574
583
|
description: "Detect whether a topic is surging/fading by claim volume (fast)",
|
|
575
584
|
specs: [{ name: "recent-days", type: "number", default: 7, desc: "Recent window in days" },
|
|
576
585
|
{ name: "baseline-days", type: "number", default: 30, desc: "Baseline window in days" },
|
|
577
|
-
{ 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],
|
|
578
588
|
run: async (args, json) => {
|
|
579
589
|
const [topic] = args.positionals;
|
|
580
590
|
if (!topic) {
|
|
581
|
-
|
|
591
|
+
printErr("Error: topic is required.");
|
|
582
592
|
process.exit(1);
|
|
583
593
|
}
|
|
584
594
|
const f = args.flags;
|
|
@@ -587,32 +597,34 @@ const COMMANDS = {
|
|
|
587
597
|
recent_days: Number(f["recent-days"]) || undefined,
|
|
588
598
|
baseline_days: Number(f["baseline-days"]) || undefined,
|
|
589
599
|
min_similarity: f["min-similarity"] !== undefined ? Number(f["min-similarity"]) : undefined,
|
|
600
|
+
language: f.language || undefined,
|
|
590
601
|
};
|
|
591
602
|
if (json)
|
|
592
|
-
return printJson(await client.detectTrendClaims(params));
|
|
603
|
+
return printJson(await client.detectTrendClaims({ ...params, project: f.collection || undefined }));
|
|
593
604
|
await runHandler("trend_claims", params);
|
|
594
605
|
},
|
|
595
606
|
},
|
|
596
607
|
"contradict-claims": {
|
|
597
608
|
tool: "contradict_claims",
|
|
598
609
|
description: "Find a speaker's contradictory claims on a topic via stance analysis (fast)",
|
|
599
|
-
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],
|
|
600
611
|
run: async (args, json) => {
|
|
601
612
|
const [topic] = args.positionals;
|
|
602
613
|
if (!topic) {
|
|
603
|
-
|
|
614
|
+
printErr("Error: topic is required.");
|
|
604
615
|
process.exit(1);
|
|
605
616
|
}
|
|
606
617
|
const f = args.flags;
|
|
607
618
|
const speaker = f.speaker || undefined;
|
|
608
619
|
if (!speaker) {
|
|
609
|
-
|
|
620
|
+
printErr("Error: --speaker is required.");
|
|
610
621
|
process.exit(1);
|
|
611
622
|
}
|
|
612
|
-
const params = { topic, speaker, days: Number(f.days) || undefined };
|
|
623
|
+
const params = { topic, speaker, days: Number(f.days) || undefined, ...dateFlags(f) };
|
|
613
624
|
if (json)
|
|
614
625
|
return printJson(await client.checkContradictionsClaims({
|
|
615
626
|
topic, speaker_name: speaker, days: params.days,
|
|
627
|
+
project: f.collection || undefined, ...dateFlags(f),
|
|
616
628
|
}));
|
|
617
629
|
await runHandler("contradict_claims", params, f.out);
|
|
618
630
|
},
|
|
@@ -620,24 +632,26 @@ const COMMANDS = {
|
|
|
620
632
|
"compare-claims": {
|
|
621
633
|
tool: "compare_claims",
|
|
622
634
|
description: "Compare 2-5 speakers' claims on a topic — side-by-side stance (fast)",
|
|
623
|
-
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],
|
|
624
637
|
run: async (args, json) => {
|
|
625
638
|
const [topic] = args.positionals;
|
|
626
639
|
if (!topic) {
|
|
627
|
-
|
|
640
|
+
printErr("Error: topic is required.");
|
|
628
641
|
process.exit(1);
|
|
629
642
|
}
|
|
630
643
|
const f = args.flags;
|
|
631
644
|
const speakersStr = f.speakers || f.speaker || "";
|
|
632
645
|
const speakers = speakersStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
633
646
|
if (speakers.length < 2) {
|
|
634
|
-
|
|
647
|
+
printErr("Error: --speakers requires 2-5 comma-separated names.");
|
|
635
648
|
process.exit(1);
|
|
636
649
|
}
|
|
637
|
-
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) };
|
|
638
651
|
if (json)
|
|
639
652
|
return printJson(await client.compareSpeakersClaims({
|
|
640
|
-
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),
|
|
641
655
|
}));
|
|
642
656
|
await runHandler("compare_claims", params, f.out);
|
|
643
657
|
},
|
|
@@ -645,24 +659,24 @@ const COMMANDS = {
|
|
|
645
659
|
"channel-dossier": {
|
|
646
660
|
tool: "channel_dossier",
|
|
647
661
|
description: "Claims dossier of what a channel (show) platformed on a topic (fast)",
|
|
648
|
-
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],
|
|
649
663
|
run: async (args, json) => {
|
|
650
664
|
const [topic] = args.positionals;
|
|
651
665
|
if (!topic) {
|
|
652
|
-
|
|
666
|
+
printErr("Error: topic is required.");
|
|
653
667
|
process.exit(1);
|
|
654
668
|
}
|
|
655
669
|
const f = args.flags;
|
|
656
670
|
const channel = f.channel || undefined;
|
|
657
671
|
if (!channel) {
|
|
658
|
-
|
|
672
|
+
printErr("Error: --channel is required.");
|
|
659
673
|
process.exit(1);
|
|
660
674
|
}
|
|
661
675
|
const channels = channel.split(",").map((s) => s.trim()).filter(Boolean);
|
|
662
676
|
const days = Number(f.days) || undefined;
|
|
663
677
|
if (json)
|
|
664
678
|
return printJson(await client.buildChannelDossier({
|
|
665
|
-
topic, channel_names: channels, days,
|
|
679
|
+
topic, channel_names: channels, days, project: f.collection || undefined,
|
|
666
680
|
}));
|
|
667
681
|
await runHandler("channel_dossier", { topic, channels, days }, f.out);
|
|
668
682
|
},
|
|
@@ -670,24 +684,24 @@ const COMMANDS = {
|
|
|
670
684
|
"compare-channels": {
|
|
671
685
|
tool: "compare_channels",
|
|
672
686
|
description: "Compare 2-5 channels' claims on a topic — stance + speaker overlap (fast)",
|
|
673
|
-
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],
|
|
674
688
|
run: async (args, json) => {
|
|
675
689
|
const [topic] = args.positionals;
|
|
676
690
|
if (!topic) {
|
|
677
|
-
|
|
691
|
+
printErr("Error: topic is required.");
|
|
678
692
|
process.exit(1);
|
|
679
693
|
}
|
|
680
694
|
const f = args.flags;
|
|
681
695
|
const channelsStr = f.channels || f.channel || "";
|
|
682
696
|
const channels = channelsStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
683
697
|
if (channels.length < 2) {
|
|
684
|
-
|
|
698
|
+
printErr("Error: --channels requires 2-5 comma-separated names.");
|
|
685
699
|
process.exit(1);
|
|
686
700
|
}
|
|
687
701
|
const days = Number(f.days) || undefined;
|
|
688
702
|
if (json)
|
|
689
703
|
return printJson(await client.compareChannelsClaims({
|
|
690
|
-
topic, channel_names: channels, days,
|
|
704
|
+
topic, channel_names: channels, days, project: f.collection || undefined,
|
|
691
705
|
}));
|
|
692
706
|
await runHandler("compare_channels", { topic, channels, days }, f.out);
|
|
693
707
|
},
|
|
@@ -695,24 +709,24 @@ const COMMANDS = {
|
|
|
695
709
|
"channel-contradictions": {
|
|
696
710
|
tool: "check_channel_contradictions",
|
|
697
711
|
description: "Contradictory claims a channel platforms on a topic (fast)",
|
|
698
|
-
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],
|
|
699
713
|
run: async (args, json) => {
|
|
700
714
|
const [topic] = args.positionals;
|
|
701
715
|
if (!topic) {
|
|
702
|
-
|
|
716
|
+
printErr("Error: topic is required.");
|
|
703
717
|
process.exit(1);
|
|
704
718
|
}
|
|
705
719
|
const f = args.flags;
|
|
706
720
|
const channel = f.channel || undefined;
|
|
707
721
|
if (!channel) {
|
|
708
|
-
|
|
722
|
+
printErr("Error: --channel is required.");
|
|
709
723
|
process.exit(1);
|
|
710
724
|
}
|
|
711
725
|
const channels = channel.split(",").map((s) => s.trim()).filter(Boolean);
|
|
712
726
|
const days = Number(f.days) || undefined;
|
|
713
727
|
if (json)
|
|
714
728
|
return printJson(await client.checkChannelContradictionsClaims({
|
|
715
|
-
topic, channel_names: channels, days,
|
|
729
|
+
topic, channel_names: channels, days, project: f.collection || undefined,
|
|
716
730
|
}));
|
|
717
731
|
await runHandler("check_channel_contradictions", { topic, channels, days }, f.out);
|
|
718
732
|
},
|
|
@@ -724,7 +738,7 @@ const COMMANDS = {
|
|
|
724
738
|
run: async (args, json) => {
|
|
725
739
|
const [query] = args.positionals;
|
|
726
740
|
if (!query) {
|
|
727
|
-
|
|
741
|
+
printErr("Error: query is required.");
|
|
728
742
|
process.exit(1);
|
|
729
743
|
}
|
|
730
744
|
const f = args.flags;
|
|
@@ -740,11 +754,11 @@ const COMMANDS = {
|
|
|
740
754
|
description: "Claims about a resolved canonical entity, ranked, with stance distribution",
|
|
741
755
|
specs: [{ name: "sort", type: "string", default: "spread", desc: "Sort: spread | observations | recent" },
|
|
742
756
|
{ name: "stance", type: "string", desc: "Filter by stance: assert | deny | speculate | attribute" },
|
|
743
|
-
FLAG_LIMIT, FLAG_OFFSET, FLAG_JSON],
|
|
757
|
+
FLAG_LIMIT, FLAG_OFFSET, FLAG_COLLECTION, FLAG_JSON],
|
|
744
758
|
run: async (args, json) => {
|
|
745
759
|
const [name] = args.positionals;
|
|
746
760
|
if (!name) {
|
|
747
|
-
|
|
761
|
+
printErr("Error: entity name is required.");
|
|
748
762
|
process.exit(1);
|
|
749
763
|
}
|
|
750
764
|
const f = args.flags;
|
|
@@ -755,6 +769,7 @@ const COMMANDS = {
|
|
|
755
769
|
if (json)
|
|
756
770
|
return printJson(await client.getEntityClaims({
|
|
757
771
|
name, sort: params.sort, stance: params.stance, limit: params.limit, offset: params.offset,
|
|
772
|
+
project: f.collection || undefined,
|
|
758
773
|
}));
|
|
759
774
|
await runHandler("entity_claims", params);
|
|
760
775
|
},
|
|
@@ -762,16 +777,16 @@ const COMMANDS = {
|
|
|
762
777
|
"claim-graph": {
|
|
763
778
|
tool: "claim_graph",
|
|
764
779
|
description: "A claim's parent, children, and sibling (variant) claims",
|
|
765
|
-
specs: [FLAG_JSON],
|
|
780
|
+
specs: [FLAG_COLLECTION, FLAG_JSON],
|
|
766
781
|
run: async (args, json) => {
|
|
767
782
|
const [id] = args.positionals;
|
|
768
783
|
if (!id || !/^\d+$/.test(id)) {
|
|
769
|
-
|
|
784
|
+
printErr("Error: claim_id (numeric) required.");
|
|
770
785
|
process.exit(1);
|
|
771
786
|
}
|
|
772
787
|
const claimId = Number(id);
|
|
773
788
|
if (json)
|
|
774
|
-
return printJson(await client.getClaimGraph(claimId));
|
|
789
|
+
return printJson(await client.getClaimGraph(claimId, args.flags.collection || undefined));
|
|
775
790
|
await runHandler("claim_graph", { claim_id: claimId });
|
|
776
791
|
},
|
|
777
792
|
},
|
|
@@ -780,16 +795,18 @@ const COMMANDS = {
|
|
|
780
795
|
description: "Leaderboard of the most widely-spread or most-contested claims",
|
|
781
796
|
specs: [{ name: "metric", type: "string", default: "spread", desc: "Ranking: spread (reach) | contested (assert vs deny balance)" },
|
|
782
797
|
{ name: "category", type: "string", desc: "Optional topic-category filter" },
|
|
783
|
-
{ 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],
|
|
784
800
|
run: async (args, json) => {
|
|
785
801
|
const f = args.flags;
|
|
786
802
|
const params = {
|
|
787
803
|
metric: f.metric || undefined, category: f.category || undefined,
|
|
788
804
|
days: Number(f.days) || undefined, min_observations: Number(f["min-obs"]) || undefined,
|
|
789
|
-
limit: Number(f.limit) || undefined,
|
|
805
|
+
limit: Number(f.limit) || undefined, language: f.language || undefined,
|
|
806
|
+
...dateFlags(f),
|
|
790
807
|
};
|
|
791
808
|
if (json)
|
|
792
|
-
return printJson(await client.getTopClaims(params));
|
|
809
|
+
return printJson(await client.getTopClaims({ ...params, project: f.collection || undefined }));
|
|
793
810
|
await runHandler("top_claims", params);
|
|
794
811
|
},
|
|
795
812
|
},
|
|
@@ -801,7 +818,7 @@ const COMMANDS = {
|
|
|
801
818
|
run: async (args, json) => {
|
|
802
819
|
const [id] = args.positionals;
|
|
803
820
|
if (!id || !/^\d+$/.test(id)) {
|
|
804
|
-
|
|
821
|
+
printErr("Error: claim_id (numeric) required.");
|
|
805
822
|
process.exit(1);
|
|
806
823
|
}
|
|
807
824
|
const claimId = Number(id);
|
|
@@ -823,15 +840,17 @@ const COMMANDS = {
|
|
|
823
840
|
trend: {
|
|
824
841
|
tool: "detect_trend",
|
|
825
842
|
description: "Detect topic volume trends",
|
|
826
|
-
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_LANGUAGE, FLAG_COUNTRY,
|
|
843
|
+
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_COLLECTION,
|
|
827
844
|
{ name: "recent", type: "number", default: 7, desc: "Recent window in days" },
|
|
828
845
|
{ name: "baseline", type: "number", default: 30, desc: "Baseline window in days" },
|
|
829
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" },
|
|
830
849
|
{ name: "min-similarity", type: "number", desc: "Minimum similarity threshold" }, FLAG_JSON],
|
|
831
850
|
run: async (args, json) => {
|
|
832
851
|
const [topic] = args.positionals;
|
|
833
852
|
if (!topic) {
|
|
834
|
-
|
|
853
|
+
printErr("Error: topic is required.");
|
|
835
854
|
process.exit(1);
|
|
836
855
|
}
|
|
837
856
|
const f = args.flags;
|
|
@@ -844,6 +863,8 @@ const COMMANDS = {
|
|
|
844
863
|
recent_days: Number(f.recent) || 7,
|
|
845
864
|
baseline_days: Number(f.days) || Number(f.baseline) || 30,
|
|
846
865
|
min_similarity: Number(f["min-similarity"]) || undefined,
|
|
866
|
+
project: f.collection || undefined,
|
|
867
|
+
...dateFlags(f),
|
|
847
868
|
};
|
|
848
869
|
if (json)
|
|
849
870
|
return printJson(await client.detectTrend({
|
|
@@ -857,18 +878,28 @@ const COMMANDS = {
|
|
|
857
878
|
tool: "get_timeline",
|
|
858
879
|
description: "Topic mention volume over time",
|
|
859
880
|
specs: [FLAG_DAYS, FLAG_SPEAKER, FLAG_CHANNEL, FLAG_LANGUAGE,
|
|
860
|
-
{ 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)" },
|
|
861
882
|
{ name: "min-similarity", type: "string", desc: "Relevance threshold 0-1 (default 0.30, same as quote search)" },
|
|
862
|
-
FLAG_START, FLAG_END, FLAG_JSON],
|
|
883
|
+
FLAG_START, FLAG_END, FLAG_COLLECTION, FLAG_JSON],
|
|
863
884
|
run: async (args, json) => {
|
|
864
885
|
const [topic] = args.positionals;
|
|
865
886
|
if (!topic) {
|
|
866
|
-
|
|
887
|
+
printErr("Error: topic is required.");
|
|
867
888
|
process.exit(1);
|
|
868
889
|
}
|
|
869
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
|
+
}
|
|
870
895
|
const days = Number(f.days) || 30;
|
|
871
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
|
+
}
|
|
872
903
|
const tlChannel = f.channel || undefined;
|
|
873
904
|
const tlParams = {
|
|
874
905
|
topic,
|
|
@@ -879,10 +910,62 @@ const COMMANDS = {
|
|
|
879
910
|
language: f.language || undefined,
|
|
880
911
|
granularity: f.granularity || "day",
|
|
881
912
|
min_similarity: f["min-similarity"] != null ? Number(f["min-similarity"]) : undefined,
|
|
913
|
+
project: f.collection || undefined,
|
|
882
914
|
};
|
|
883
915
|
if (json)
|
|
884
916
|
return printJson(await client.getTimeline(tlParams));
|
|
885
|
-
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);
|
|
886
969
|
},
|
|
887
970
|
},
|
|
888
971
|
speaker: {
|
|
@@ -893,7 +976,7 @@ const COMMANDS = {
|
|
|
893
976
|
run: async (args, json) => {
|
|
894
977
|
const [identity, topic] = args.positionals;
|
|
895
978
|
if (!identity) {
|
|
896
|
-
|
|
979
|
+
printErr("Error: speaker name or ID required.");
|
|
897
980
|
process.exit(1);
|
|
898
981
|
}
|
|
899
982
|
const f = args.flags;
|
|
@@ -966,7 +1049,7 @@ const COMMANDS = {
|
|
|
966
1049
|
run: async (args, json) => {
|
|
967
1050
|
const [id] = args.positionals;
|
|
968
1051
|
if (!id) {
|
|
969
|
-
|
|
1052
|
+
printErr("Error: channel ID or name required.");
|
|
970
1053
|
process.exit(1);
|
|
971
1054
|
}
|
|
972
1055
|
const isId = /^\d+$/.test(id);
|
|
@@ -1001,7 +1084,7 @@ const COMMANDS = {
|
|
|
1001
1084
|
run: async (args, json) => {
|
|
1002
1085
|
const [id] = args.positionals;
|
|
1003
1086
|
if (!id) {
|
|
1004
|
-
|
|
1087
|
+
printErr("Error: content ID required.");
|
|
1005
1088
|
process.exit(1);
|
|
1006
1089
|
}
|
|
1007
1090
|
if (json) {
|
|
@@ -1020,7 +1103,7 @@ const COMMANDS = {
|
|
|
1020
1103
|
run: async (args, json) => {
|
|
1021
1104
|
const [id] = args.positionals;
|
|
1022
1105
|
if (!id) {
|
|
1023
|
-
|
|
1106
|
+
printErr("Error: segment ID required.");
|
|
1024
1107
|
process.exit(1);
|
|
1025
1108
|
}
|
|
1026
1109
|
const segId = parseSegmentId(id);
|
|
@@ -1032,15 +1115,15 @@ const COMMANDS = {
|
|
|
1032
1115
|
thread: {
|
|
1033
1116
|
tool: "get_thread",
|
|
1034
1117
|
description: "Conversation context around a quote",
|
|
1035
|
-
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],
|
|
1036
1119
|
run: async (args, json) => {
|
|
1037
1120
|
const [id] = args.positionals;
|
|
1038
1121
|
if (!id) {
|
|
1039
|
-
|
|
1122
|
+
printErr("Error: segment ID required.");
|
|
1040
1123
|
process.exit(1);
|
|
1041
1124
|
}
|
|
1042
1125
|
const segId = parseSegmentId(id);
|
|
1043
|
-
const n = Number(args.flags.
|
|
1126
|
+
const n = Number(args.flags.context) || 5;
|
|
1044
1127
|
if (json)
|
|
1045
1128
|
return printJson(await client.getSegmentThread(segId, n));
|
|
1046
1129
|
await runHandler("get_thread", { segment_id: segId, n });
|
|
@@ -1049,13 +1132,13 @@ const COMMANDS = {
|
|
|
1049
1132
|
analyze: {
|
|
1050
1133
|
tool: "analyze_topic",
|
|
1051
1134
|
description: "AI-powered topic analysis",
|
|
1052
|
-
specs: [FLAG_SPEAKER, FLAG_CHANNEL, FLAG_DAYS, FLAG_LANGUAGE, FLAG_COUNTRY,
|
|
1053
|
-
{ 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" },
|
|
1054
1137
|
FLAG_SKIP_CACHE, FLAG_OUT, FLAG_JSON],
|
|
1055
1138
|
run: async (args, json) => {
|
|
1056
1139
|
const [query] = args.positionals;
|
|
1057
1140
|
if (!query) {
|
|
1058
|
-
|
|
1141
|
+
printErr("Error: query is required.");
|
|
1059
1142
|
process.exit(1);
|
|
1060
1143
|
}
|
|
1061
1144
|
const f = args.flags;
|
|
@@ -1069,6 +1152,7 @@ const COMMANDS = {
|
|
|
1069
1152
|
country: f.country || undefined,
|
|
1070
1153
|
days: Number(f.days) || undefined,
|
|
1071
1154
|
skip_cache: f["skip-cache"],
|
|
1155
|
+
project: f.collection || undefined,
|
|
1072
1156
|
};
|
|
1073
1157
|
if (json)
|
|
1074
1158
|
return printJson(await client.analyze(params));
|
|
@@ -1078,11 +1162,11 @@ const COMMANDS = {
|
|
|
1078
1162
|
frames: {
|
|
1079
1163
|
tool: "compare_frames",
|
|
1080
1164
|
description: "Compare left vs right media framing on a topic",
|
|
1081
|
-
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],
|
|
1082
1166
|
run: async (args, json) => {
|
|
1083
1167
|
const [topic] = args.positionals;
|
|
1084
1168
|
if (!topic) {
|
|
1085
|
-
|
|
1169
|
+
printErr("Error: topic is required.");
|
|
1086
1170
|
process.exit(1);
|
|
1087
1171
|
}
|
|
1088
1172
|
const f = args.flags;
|
|
@@ -1094,7 +1178,7 @@ const COMMANDS = {
|
|
|
1094
1178
|
skip_cache: f["skip-cache"],
|
|
1095
1179
|
};
|
|
1096
1180
|
if (json) {
|
|
1097
|
-
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 };
|
|
1098
1182
|
const [left, right] = await Promise.all([
|
|
1099
1183
|
client.analyze({ ...shared, political_lean_min: 0.0, political_lean_max: 0.35 }),
|
|
1100
1184
|
client.analyze({ ...shared, political_lean_min: 0.65, political_lean_max: 1.0 }),
|
|
@@ -1109,18 +1193,18 @@ const COMMANDS = {
|
|
|
1109
1193
|
description: "Compare speakers on a topic",
|
|
1110
1194
|
// FLAG_DAYS carries default:30, which silently overrode this command's
|
|
1111
1195
|
// intended 90d window — spread it with the right default.
|
|
1112
|
-
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],
|
|
1113
1197
|
run: async (args, json) => {
|
|
1114
1198
|
const [topic] = args.positionals;
|
|
1115
1199
|
if (!topic) {
|
|
1116
|
-
|
|
1200
|
+
printErr("Error: topic is required.");
|
|
1117
1201
|
process.exit(1);
|
|
1118
1202
|
}
|
|
1119
1203
|
const f = args.flags;
|
|
1120
1204
|
const speakersStr = f.speakers || f.speaker || "";
|
|
1121
1205
|
const speakers = speakersStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1122
1206
|
if (speakers.length < 2) {
|
|
1123
|
-
|
|
1207
|
+
printErr("Error: --speakers requires 2-5 comma-separated names.");
|
|
1124
1208
|
process.exit(1);
|
|
1125
1209
|
}
|
|
1126
1210
|
process.stderr.write("Comparing speakers... (may take 1-3 minutes)\n");
|
|
@@ -1134,7 +1218,7 @@ const COMMANDS = {
|
|
|
1134
1218
|
return printJson(await client.analyze({
|
|
1135
1219
|
query: topic, workflow: "speaker_compare", speaker_names: speakers,
|
|
1136
1220
|
language: params.language, country: params.country,
|
|
1137
|
-
days: params.days, skip_cache: params.skip_cache,
|
|
1221
|
+
days: params.days, skip_cache: params.skip_cache, project: f.collection || undefined,
|
|
1138
1222
|
}));
|
|
1139
1223
|
await runHandler("compare_speakers", params, f.out);
|
|
1140
1224
|
},
|
|
@@ -1151,14 +1235,14 @@ const COMMANDS = {
|
|
|
1151
1235
|
run: async (args, json) => {
|
|
1152
1236
|
const [topic] = args.positionals;
|
|
1153
1237
|
if (!topic) {
|
|
1154
|
-
|
|
1238
|
+
printErr("Error: topic is required.");
|
|
1155
1239
|
process.exit(1);
|
|
1156
1240
|
}
|
|
1157
1241
|
if (args.flags.speakers)
|
|
1158
1242
|
return runDossierBatch(topic, args, json);
|
|
1159
1243
|
const speaker = args.flags.speaker;
|
|
1160
1244
|
if (!speaker) {
|
|
1161
|
-
|
|
1245
|
+
printErr("Error: --speaker is required (or --speakers <file> for batch mode).");
|
|
1162
1246
|
process.exit(1);
|
|
1163
1247
|
}
|
|
1164
1248
|
process.stderr.write("Building dossier... (may take 1-3 minutes)\n");
|
|
@@ -1179,18 +1263,18 @@ const COMMANDS = {
|
|
|
1179
1263
|
run: async (args, json) => {
|
|
1180
1264
|
const [topic] = args.positionals;
|
|
1181
1265
|
if (!topic) {
|
|
1182
|
-
|
|
1266
|
+
printErr("Error: topic is required.");
|
|
1183
1267
|
process.exit(1);
|
|
1184
1268
|
}
|
|
1185
1269
|
const f = args.flags;
|
|
1186
1270
|
const speaker = f.speaker;
|
|
1187
1271
|
if (!speaker) {
|
|
1188
|
-
|
|
1272
|
+
printErr("Error: --speaker is required.");
|
|
1189
1273
|
process.exit(1);
|
|
1190
1274
|
}
|
|
1191
1275
|
const periods = (f.periods || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1192
1276
|
if (periods.length < 2) {
|
|
1193
|
-
|
|
1277
|
+
printErr("Error: --periods requires 2+ comma-separated periods (e.g. 2024,2026).");
|
|
1194
1278
|
process.exit(1);
|
|
1195
1279
|
}
|
|
1196
1280
|
process.stderr.write("Comparing periods... (may take 1-3 minutes)\n");
|
|
@@ -1201,7 +1285,7 @@ const COMMANDS = {
|
|
|
1201
1285
|
if (json)
|
|
1202
1286
|
return printJson(data);
|
|
1203
1287
|
if (data.error) {
|
|
1204
|
-
|
|
1288
|
+
printErr(`Error: ${data.error}`);
|
|
1205
1289
|
process.exit(1);
|
|
1206
1290
|
}
|
|
1207
1291
|
const lines = [`**Speaker evolution: ${speaker} on ${topic} (${periods.join(" vs ")})**\n`];
|
|
@@ -1234,12 +1318,12 @@ const COMMANDS = {
|
|
|
1234
1318
|
run: async (args, json) => {
|
|
1235
1319
|
const [topic] = args.positionals;
|
|
1236
1320
|
if (!topic) {
|
|
1237
|
-
|
|
1321
|
+
printErr("Error: topic is required.");
|
|
1238
1322
|
process.exit(1);
|
|
1239
1323
|
}
|
|
1240
1324
|
const speaker = args.flags.speaker;
|
|
1241
1325
|
if (!speaker) {
|
|
1242
|
-
|
|
1326
|
+
printErr("Error: --speaker is required.");
|
|
1243
1327
|
process.exit(1);
|
|
1244
1328
|
}
|
|
1245
1329
|
process.stderr.write("Checking contradictions... (may take 1-3 minutes)\n");
|
|
@@ -1254,22 +1338,32 @@ const COMMANDS = {
|
|
|
1254
1338
|
briefing: {
|
|
1255
1339
|
tool: "get_briefing",
|
|
1256
1340
|
description: "AI discourse briefing",
|
|
1257
|
-
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],
|
|
1258
1342
|
run: async (args, json) => {
|
|
1259
1343
|
const f = args.flags;
|
|
1260
1344
|
const params = { days: Number(f.days) || 3, max_topics: Number(f["max-topics"]) || 8 };
|
|
1261
1345
|
if (json)
|
|
1262
|
-
return printJson(await client.getDiscourseSummary(params));
|
|
1346
|
+
return printJson(await client.getDiscourseSummary({ ...params, project: f.collection || undefined }));
|
|
1263
1347
|
await runHandler("get_briefing", params);
|
|
1264
1348
|
},
|
|
1265
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
|
+
},
|
|
1266
1360
|
stats: {
|
|
1267
1361
|
tool: "get_platform_stats",
|
|
1268
1362
|
description: "Platform statistics",
|
|
1269
|
-
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],
|
|
1270
1364
|
run: async (args, json) => {
|
|
1271
1365
|
const f = args.flags;
|
|
1272
|
-
if (f.
|
|
1366
|
+
if (f.collections) {
|
|
1273
1367
|
if (json)
|
|
1274
1368
|
return printJson(await client.getProjectStats());
|
|
1275
1369
|
await runHandler("get_project_stats", {});
|
|
@@ -1285,23 +1379,23 @@ const COMMANDS = {
|
|
|
1285
1379
|
"top-channels": {
|
|
1286
1380
|
tool: "get_top_channels",
|
|
1287
1381
|
description: "Top channels by episode count",
|
|
1288
|
-
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],
|
|
1289
1383
|
run: async (args, json) => {
|
|
1290
1384
|
const f = args.flags;
|
|
1291
|
-
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) };
|
|
1292
1386
|
if (json)
|
|
1293
|
-
return printJson(await client.getTopChannels(params));
|
|
1387
|
+
return printJson(await client.getTopChannels({ ...params, project: f.collection || undefined }));
|
|
1294
1388
|
await runHandler("get_top_channels", params);
|
|
1295
1389
|
},
|
|
1296
1390
|
},
|
|
1297
1391
|
"top-speakers": {
|
|
1298
1392
|
tool: "get_top_speakers",
|
|
1299
1393
|
description: "Top speakers by appearances, talk time, or clips",
|
|
1300
|
-
specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_LANGUAGE, FLAG_COUNTRY,
|
|
1301
|
-
{ 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],
|
|
1302
1396
|
run: async (args, json) => {
|
|
1303
1397
|
const f = args.flags;
|
|
1304
|
-
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.
|
|
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) };
|
|
1305
1399
|
if (json)
|
|
1306
1400
|
return printJson(await client.getTopSpeakers(params));
|
|
1307
1401
|
await runHandler("get_top_speakers", params);
|
|
@@ -1310,10 +1404,10 @@ const COMMANDS = {
|
|
|
1310
1404
|
network: {
|
|
1311
1405
|
tool: "get_speaker_network",
|
|
1312
1406
|
description: "Speaker co-appearance network",
|
|
1313
|
-
specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_LANGUAGE,
|
|
1407
|
+
specs: [FLAG_DAYS, FLAG_LIMIT, FLAG_LANGUAGE, FLAG_COLLECTION, FLAG_JSON],
|
|
1314
1408
|
run: async (args, json) => {
|
|
1315
1409
|
const f = args.flags;
|
|
1316
|
-
const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 20, language: f.language || "en", project: f.
|
|
1410
|
+
const params = { days: Number(f.days) || 30, limit: Number(f.limit) || 20, language: f.language || "en", project: f.collection || undefined };
|
|
1317
1411
|
if (json)
|
|
1318
1412
|
return printJson(await client.getSpeakerNetwork(params));
|
|
1319
1413
|
await runHandler("get_speaker_network", params);
|
|
@@ -1323,7 +1417,7 @@ const COMMANDS = {
|
|
|
1323
1417
|
tool: "get_topics",
|
|
1324
1418
|
description: "Trending topic clusters",
|
|
1325
1419
|
specs: [FLAG_LANGUAGE, FLAG_COUNTRY, FLAG_LIMIT, FLAG_DAYS,
|
|
1326
|
-
{ name: "query",
|
|
1420
|
+
{ name: "query", type: "string", desc: "Semantic filter — only clusters relevant to this query" },
|
|
1327
1421
|
{ name: "min-segments", type: "number", default: 50, desc: "Minimum segments per cluster" },
|
|
1328
1422
|
FLAG_START, FLAG_END, FLAG_JSON],
|
|
1329
1423
|
run: async (args, json) => {
|
|
@@ -1382,7 +1476,7 @@ const COMMANDS = {
|
|
|
1382
1476
|
const profile = await client.getChannelByName(channelArg);
|
|
1383
1477
|
const resolvedId = profile.channel_id ?? profile.id;
|
|
1384
1478
|
if (!resolvedId) {
|
|
1385
|
-
|
|
1479
|
+
printErr(`Error: channel "${channelArg}" not found.`);
|
|
1386
1480
|
process.exit(1);
|
|
1387
1481
|
}
|
|
1388
1482
|
if (json)
|
|
@@ -1452,7 +1546,7 @@ async function cmdSetup(force) {
|
|
|
1452
1546
|
const key = await rl.question("API key: ");
|
|
1453
1547
|
rl.close();
|
|
1454
1548
|
if (!key.trim()) {
|
|
1455
|
-
|
|
1549
|
+
printErr("Error: no API key provided.");
|
|
1456
1550
|
process.exit(1);
|
|
1457
1551
|
}
|
|
1458
1552
|
process.env.SIGNAL4_API_KEY = key.trim();
|
|
@@ -1461,7 +1555,7 @@ async function cmdSetup(force) {
|
|
|
1461
1555
|
print(`Authenticated as ${who.user_email ?? who.key_prefix ?? "unknown"}`);
|
|
1462
1556
|
}
|
|
1463
1557
|
catch (err) {
|
|
1464
|
-
|
|
1558
|
+
printErr(`Error: invalid API key (${err instanceof Error ? err.message : String(err)})`);
|
|
1465
1559
|
process.exit(1);
|
|
1466
1560
|
}
|
|
1467
1561
|
if (!existsSync(configDir))
|
|
@@ -1553,7 +1647,7 @@ async function showHelp() {
|
|
|
1553
1647
|
lines.push(`\u2500\u2500 ${tier} \u2500\u2500`);
|
|
1554
1648
|
for (const c of cmds) {
|
|
1555
1649
|
shown.add(c.name);
|
|
1556
|
-
lines.push(` ${c.name.padEnd(19)}${c.desc}`);
|
|
1650
|
+
lines.push(` ${c.name.padEnd(19)}${c.desc}${speedWord(c.name)}`);
|
|
1557
1651
|
}
|
|
1558
1652
|
lines.push("");
|
|
1559
1653
|
}
|
|
@@ -1564,7 +1658,7 @@ async function showHelp() {
|
|
|
1564
1658
|
lines.push(`\u2500\u2500 ${tier} \u2500\u2500`);
|
|
1565
1659
|
for (const c of cmds) {
|
|
1566
1660
|
shown.add(c.name);
|
|
1567
|
-
lines.push(` ${c.name.padEnd(19)}${c.desc}`);
|
|
1661
|
+
lines.push(` ${c.name.padEnd(19)}${c.desc}${speedWord(c.name)}`);
|
|
1568
1662
|
}
|
|
1569
1663
|
lines.push("");
|
|
1570
1664
|
}
|
|
@@ -1577,7 +1671,7 @@ async function showHelp() {
|
|
|
1577
1671
|
lines.push("\u2500\u2500 also available \u2500\u2500");
|
|
1578
1672
|
for (const n of missing) {
|
|
1579
1673
|
const pad = n.length >= 19 ? `${n} ` : n.padEnd(19);
|
|
1580
|
-
lines.push(` ${pad}${COMMANDS[n].description}`);
|
|
1674
|
+
lines.push(` ${pad}${COMMANDS[n].description}${speedWord(n)}`);
|
|
1581
1675
|
}
|
|
1582
1676
|
lines.push("");
|
|
1583
1677
|
}
|
|
@@ -1596,11 +1690,14 @@ async function showHelp() {
|
|
|
1596
1690
|
lines.push(` ${data.available_commands.join(", ")}`);
|
|
1597
1691
|
lines.push("");
|
|
1598
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("");
|
|
1599
1695
|
lines.push("Flags:");
|
|
1600
1696
|
lines.push(" --json Raw JSON output");
|
|
1601
1697
|
lines.push(" --help, -h Show this help");
|
|
1602
1698
|
lines.push(" s4 <command> --help Flags for one command (flags vary by command)");
|
|
1603
|
-
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");
|
|
1604
1701
|
lines.push("");
|
|
1605
1702
|
lines.push("MCP server:");
|
|
1606
1703
|
lines.push(" s4 --mcp Start MCP server for Claude Desktop / Cursor");
|
|
@@ -1615,10 +1712,17 @@ async function showHelp() {
|
|
|
1615
1712
|
].join("\n"));
|
|
1616
1713
|
}
|
|
1617
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
|
+
}
|
|
1618
1720
|
function showCommandHelp(name, cmd) {
|
|
1619
1721
|
const lines = [
|
|
1620
1722
|
`s4 ${name} — ${cmd.description}`,
|
|
1621
1723
|
"",
|
|
1724
|
+
`Speed: ${descriptionTag(cmd.tool).slice(1, -1)} (instant <0.5s · fast 0.5–2.5s · slow >2.5s)`,
|
|
1725
|
+
"",
|
|
1622
1726
|
`Usage: s4 ${name} [args] [flags]`,
|
|
1623
1727
|
"",
|
|
1624
1728
|
"Flags:",
|
|
@@ -1635,12 +1739,35 @@ function showCommandHelp(name, cmd) {
|
|
|
1635
1739
|
// ---------------------------------------------------------------------------
|
|
1636
1740
|
// Main
|
|
1637
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"];
|
|
1638
1760
|
export async function main() {
|
|
1639
1761
|
applyConfig();
|
|
1762
|
+
setSurface("cli");
|
|
1640
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
|
+
}
|
|
1641
1768
|
// Global help — but `s4 <command> --help` gets that command's flags instead.
|
|
1642
1769
|
const wantsHelp = argv.includes("--help") || argv.includes("-h");
|
|
1643
|
-
if (!argv.length || (wantsHelp && !COMMANDS[argv[0]])) {
|
|
1770
|
+
if (!argv.length || argv[0] === "help" || (wantsHelp && !COMMANDS[argv[0]])) {
|
|
1644
1771
|
await showHelp();
|
|
1645
1772
|
process.exit(0);
|
|
1646
1773
|
}
|
|
@@ -1653,6 +1780,8 @@ export async function main() {
|
|
|
1653
1780
|
await import("./index.js");
|
|
1654
1781
|
return;
|
|
1655
1782
|
}
|
|
1783
|
+
// CLI mode gets its own UA (MCP entry points keep the default).
|
|
1784
|
+
client.setUserAgent("Signal4-S4CLI/2.7");
|
|
1656
1785
|
const command = argv[0];
|
|
1657
1786
|
const rest = argv.slice(1);
|
|
1658
1787
|
// Setup (no API key needed)
|
|
@@ -1673,12 +1802,24 @@ export async function main() {
|
|
|
1673
1802
|
// Lookup command
|
|
1674
1803
|
const cmd = COMMANDS[command];
|
|
1675
1804
|
if (!cmd) {
|
|
1676
|
-
|
|
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`);
|
|
1677
1807
|
process.exit(1);
|
|
1678
1808
|
}
|
|
1679
1809
|
// Parse args
|
|
1680
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
|
+
}
|
|
1681
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
|
+
}
|
|
1682
1823
|
// Narrate pipeline steps on stderr while analytical commands stream.
|
|
1683
1824
|
let progress = null;
|
|
1684
1825
|
if (PROGRESS_COMMANDS.has(command)) {
|