@insightsentry/mcp 1.4.39 → 1.4.40

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/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execFile } from "node:child_process";
2
+ import { access } from "node:fs/promises";
2
3
  import { stdin as input, stdout as output } from "node:process";
3
4
  import { emitKeypressEvents } from "node:readline";
4
5
  import { createInterface } from "node:readline/promises";
@@ -14,15 +15,20 @@ import { downloadHistory, validateOutputDirectory, } from "./history.js";
14
15
  import { validateHistoryIntervalArgs } from "./history-validation.js";
15
16
  import { PACKAGE_JSON } from "./package-info.js";
16
17
  import { supportsCsvStorage, validateResponseStorageTarget, } from "./response-storage.js";
18
+ import { createStreamOutput, } from "./stream-output.js";
17
19
  import { analyzeSymbolCodes, shouldPromptSymbolScopedParam } from "./symbol-param-applicability.js";
18
20
  import { validateSymbolLikeArg } from "./symbol-validation.js";
19
21
  import { toolDefinitions } from "./tool-definitions.js";
20
22
  import { runApiTool, validateFilterExpression } from "./tool-runner.js";
21
23
  import { fetchLatestPackageVersion, formatUpgradeNotice, formatVersionStatus, getVersionStatus, } from "./version-status.js";
24
+ import { buildLiveStreamRequest, buildNewsfeedStreamRequest, } from "./websocket-contract.js";
25
+ import { runWebSocketStream, } from "./websocket-stream.js";
22
26
  export { coerceArgs } from "./arg-coercion.js";
23
27
  const DOWNLOAD_HISTORY_COMMAND = "download_history";
24
28
  const GET_SYMBOL_HISTORY_COMMAND = "get_symbol_history";
25
29
  const RENDER_CHART_COMMAND = "render_chart";
30
+ const STREAM_LIVE_COMMAND = "stream_live";
31
+ const STREAM_NEWSFEED_COMMAND = "stream_newsfeed";
26
32
  const UPDATE_COMMAND = "update";
27
33
  const VERSION_COMMAND = "version";
28
34
  const MAX_INTERACTIVE_PROMPT_ATTEMPTS = 3;
@@ -69,8 +75,86 @@ const RENDER_CHART_SCHEMA = {
69
75
  .optional()
70
76
  .describe("Chart height in pixels. Default: 400."),
71
77
  };
78
+ const LIVE_STREAM_SCHEMA = {
79
+ symbols: z
80
+ .string()
81
+ .optional()
82
+ .describe("Comma-separated EXCHANGE:SYMBOL codes for simple subscriptions."),
83
+ type: z
84
+ .enum(["quote", "quotes", "series", "both"])
85
+ .optional()
86
+ .describe("Data to receive for every symbol. quotes is accepted as an alias for quote."),
87
+ subscriptions: z
88
+ .string()
89
+ .optional()
90
+ .describe("JSON array of subscription objects for different per-symbol settings."),
91
+ bar_type: z
92
+ .enum(["tick", "second", "minute", "hour", "day", "week", "month"])
93
+ .optional()
94
+ .describe("Series bar type."),
95
+ bar_interval: z
96
+ .number()
97
+ .int()
98
+ .positive()
99
+ .optional()
100
+ .describe("Series interval. Tick requires 1; second accepts 1, 5, 10, 15, 30, or 45."),
101
+ dp: z
102
+ .number()
103
+ .int()
104
+ .min(1)
105
+ .max(30_000)
106
+ .optional()
107
+ .describe("Initial series data points, 1-30000."),
108
+ currency: z.string().optional().describe("Requested currency code."),
109
+ unit: z.string().optional().describe("Requested unit code."),
110
+ extended: z.boolean().optional().describe("Include extended-hours data."),
111
+ "24h": z.boolean().optional().describe("Include 24-hour market data."),
112
+ split: z.boolean().optional().describe("Apply split adjustment."),
113
+ dadj: z.boolean().optional().describe("Apply dividend adjustment."),
114
+ badj: z.boolean().optional().describe("Back-adjust continuous futures."),
115
+ settlement: z.boolean().optional().describe("Use settlement as daily close."),
116
+ slow_consumer_policy: z
117
+ .enum(["disconnect", "adaptive"])
118
+ .optional()
119
+ .describe("Use adaptive for fresh lower-frequency delivery instead of backlog disconnects."),
120
+ };
121
+ const NEWSFEED_STREAM_SCHEMA = {
122
+ source: z.string().optional().describe("Case-insensitive exact source name."),
123
+ related_symbols: z.string().optional().describe("Comma- or space-separated related symbols."),
124
+ content: z
125
+ .string()
126
+ .optional()
127
+ .describe("Comma- or space-separated terms; matches when content contains at least one term."),
128
+ };
129
+ const STREAM_OUTPUT_SCHEMA = {
130
+ output_file: z.string().optional().describe("Write NDJSON messages to this file."),
131
+ append: z
132
+ .boolean()
133
+ .optional()
134
+ .describe("Append instead of replacing the output file. Requires output_file."),
135
+ tee: z.boolean().optional().describe("Also write NDJSON to stdout when output_file is set."),
136
+ max_reconnect_attempts: z
137
+ .number()
138
+ .int()
139
+ .min(0)
140
+ .max(100)
141
+ .optional()
142
+ .describe("Maximum consecutive reconnect or request-retry attempts. Use 0 to disable automatic reconnects. Default: 10."),
143
+ duration_seconds: z
144
+ .number()
145
+ .positive()
146
+ .optional()
147
+ .describe("Stop successfully after this total duration, including reconnects."),
148
+ max_messages: z
149
+ .number()
150
+ .int()
151
+ .positive()
152
+ .optional()
153
+ .describe("Stop successfully after this many data messages; status and time frames do not count."),
154
+ };
72
155
  export function parseArgs(argv) {
73
156
  const args = {};
157
+ const valuelessArgs = new Set();
74
158
  let toolName = null;
75
159
  let help = false;
76
160
  let version = false;
@@ -97,10 +181,12 @@ export function parseArgs(argv) {
97
181
  const next = argv[i + 1];
98
182
  if (next === undefined || next.startsWith("--")) {
99
183
  args[key] = "true";
184
+ valuelessArgs.add(key);
100
185
  i++;
101
186
  }
102
187
  else {
103
188
  args[key] = next;
189
+ valuelessArgs.delete(key);
104
190
  i += 2;
105
191
  }
106
192
  }
@@ -108,7 +194,7 @@ export function parseArgs(argv) {
108
194
  i++;
109
195
  }
110
196
  }
111
- return { toolName, args, help, version };
197
+ return { toolName, args, valuelessArgs: [...valuelessArgs], help, version };
112
198
  }
113
199
  export function buildVersion() {
114
200
  return `${PACKAGE_JSON.name} ${PACKAGE_JSON.version}`;
@@ -130,6 +216,8 @@ export function buildHelp() {
130
216
  lines.push(` ${DOWNLOAD_HISTORY_COMMAND.padEnd(32)} Download historical ranges to JSON/CSV files with concurrency and progress`);
131
217
  lines.push(` ${GET_SYMBOL_HISTORY_COMMAND.padEnd(32)} Same as download_history; downloads history to files`);
132
218
  lines.push(` ${RENDER_CHART_COMMAND.padEnd(32)} Render Chart.js configs as PNG images`);
219
+ lines.push(` ${STREAM_LIVE_COMMAND.padEnd(32)} Stream real-time market data as NDJSON`);
220
+ lines.push(` ${STREAM_NEWSFEED_COMMAND.padEnd(32)} Stream financial news as NDJSON`);
133
221
  lines.push("");
134
222
  lines.push("Quick Start:");
135
223
  lines.push(' insight search_symbols --query "apple"');
@@ -139,6 +227,8 @@ export function buildHelp() {
139
227
  lines.push(' insight screen_stocks --fields "close,volume,market_cap" --exchanges "NYSE,NASDAQ" --sortBy market_cap --sortOrder desc');
140
228
  lines.push(" insight get_earnings --c US");
141
229
  lines.push(' insight get_options_contracts --code "NASDAQ:AAPL" --type call --range 10');
230
+ lines.push(' insight stream_live --symbols "NASDAQ:AAPL" --type both --bar_type minute');
231
+ lines.push(" insight stream_newsfeed --output_file ./news.ndjson --append");
142
232
  lines.push("");
143
233
  lines.push("API tools support --filter <jsonata> to transform the response.");
144
234
  lines.push("Use: insight <tool> --help for tool-specific parameters.");
@@ -201,6 +291,54 @@ export function buildRenderChartHelp() {
201
291
  ' insight render_chart --config \'{"type":"bar","data":{"labels":["Jan"],"datasets":[{"label":"Volume","data":[1200]}]}}\' --width 1200 --height 600',
202
292
  ].join("\n");
203
293
  }
294
+ export function buildStreamHelp(commandName) {
295
+ const isLive = commandName === STREAM_LIVE_COMMAND;
296
+ const schema = isLive
297
+ ? { ...LIVE_STREAM_SCHEMA, ...STREAM_OUTPUT_SCHEMA }
298
+ : { ...NEWSFEED_STREAM_SCHEMA, ...STREAM_OUTPUT_SCHEMA };
299
+ const lines = [
300
+ `insight ${commandName}`,
301
+ "",
302
+ isLive
303
+ ? "Stream real-time quotes and series as newline-delimited JSON. Use --symbols with --type for common settings, or --subscriptions for per-symbol settings."
304
+ : "Stream matching financial news as newline-delimited JSON.",
305
+ ];
306
+ if (isLive) {
307
+ lines.push("", "Subscription modes:", " Simple: --symbols <codes> --type <quote|series|both>", " Per-symbol: --subscriptions <json-array>");
308
+ }
309
+ lines.push("", "Parameters:");
310
+ for (const [key, zodType] of Object.entries(schema)) {
311
+ const requirement = isLive && key === "symbols"
312
+ ? "[required with --type]"
313
+ : isLive && key === "type"
314
+ ? "[required with --symbols]"
315
+ : isLive && key === "subscriptions"
316
+ ? "[alternative to --symbols/--type]"
317
+ : "[optional]";
318
+ lines.push(` --${key.padEnd(24)} ${formatTypeName(zodType)} ${requirement} ${formatHelpDescription(zodType)}`);
319
+ }
320
+ lines.push("", "Boolean options accept a bare flag or an explicit true/false value.", "Received WebSocket frames use stdout as NDJSON; CLI status and local errors use stderr.");
321
+ if (isLive) {
322
+ lines.push("Quote updates can be sparse; merge updates by code when maintaining a current snapshot.");
323
+ }
324
+ else {
325
+ lines.push("Populated filter fields are combined, so a news item must satisfy every populated field.", "Each initial connection and reconnection replays up to 10 recent matching news items before new items.");
326
+ }
327
+ lines.push("");
328
+ lines.push("Examples:");
329
+ if (isLive) {
330
+ lines.push(' insight stream_live --symbols "NASDAQ:AAPL,NASDAQ:MSFT" --type both --bar_type minute --bar_interval 1');
331
+ lines.push(` insight stream_live --subscriptions '[{"code":"NASDAQ:AAPL","type":"series","bar_type":"day"},{"code":"NASDAQ:MSFT","type":"quote"}]' --output_file ./market.ndjson --append`);
332
+ }
333
+ else {
334
+ lines.push(" insight stream_newsfeed");
335
+ lines.push(' insight stream_newsfeed --content "earnings" --related_symbols "NASDAQ:AAPL" --output_file ./news.ndjson --append');
336
+ }
337
+ lines.push("");
338
+ lines.push("Press Ctrl+C to close the socket after queued output is written.");
339
+ lines.push("Docs: https://insightsentry.com/docs/ws");
340
+ return lines.join("\n");
341
+ }
204
342
  const toolExamples = {
205
343
  search_symbols: [
206
344
  'insight search_symbols --query "tesla"',
@@ -393,7 +531,7 @@ function isPromptAbortError(error) {
393
531
  return candidate.code === "ABORT_ERR" || candidate.name === "AbortError";
394
532
  }
395
533
  async function runCliInner(argv, io) {
396
- let { toolName, args, help, version } = parseArgs(argv);
534
+ let { toolName, args, valuelessArgs, help, version } = parseArgs(argv);
397
535
  let sessionApiKey;
398
536
  if (version) {
399
537
  io.write(buildVersion());
@@ -466,6 +604,15 @@ async function runCliInner(argv, io) {
466
604
  }
467
605
  return;
468
606
  }
607
+ if (isStreamCommand(toolName)) {
608
+ if (help) {
609
+ io.write(buildStreamHelp(toolName));
610
+ io.exit(0);
611
+ return;
612
+ }
613
+ await runStreamCliCommand(toolName, args, valuelessArgs, sessionApiKey, io);
614
+ return;
615
+ }
469
616
  if (isHistoryDownloadCommand(toolName)) {
470
617
  if (help) {
471
618
  io.write(buildDownloadHistoryHelp(toolName));
@@ -701,6 +848,304 @@ async function resolveInteractiveToolName(io) {
701
848
  io.write("No tool selected.");
702
849
  return null;
703
850
  }
851
+ async function runStreamCliCommand(commandName, args, valuelessArgs, sessionApiKey, io) {
852
+ const inputSchema = commandName === STREAM_LIVE_COMMAND ? LIVE_STREAM_SCHEMA : NEWSFEED_STREAM_SCHEMA;
853
+ const fullSchema = { ...inputSchema, ...STREAM_OUTPUT_SCHEMA };
854
+ const inputArgs = { ...args };
855
+ const guided = io.isInteractive === true && Boolean(io.prompt) && Object.keys(args).length === 0;
856
+ const unknownArg = Object.keys(inputArgs).find((key) => !(key in fullSchema));
857
+ if (unknownArg) {
858
+ const suggestion = closestStreamOption(unknownArg, Object.keys(fullSchema));
859
+ writeStreamError(io, `Error: Unknown option --${unknownArg}.${suggestion ? ` Did you mean --${suggestion}?` : ` Run: insight ${commandName} --help`}`);
860
+ io.exit(1);
861
+ return;
862
+ }
863
+ const validationErrors = collectInvalidStreamArgs(inputArgs, fullSchema, valuelessArgs);
864
+ if (validationErrors.length > 0) {
865
+ const validationError = validationErrors[0];
866
+ writeStreamError(io, `Invalid ${toolPromptLabel(validationError.key)}: ${validationError.error}`);
867
+ io.exit(1);
868
+ return;
869
+ }
870
+ const resolvedArgs = guided ? await resolveStreamArgs(commandName, inputArgs, io) : inputArgs;
871
+ if (!resolvedArgs) {
872
+ writeStreamError(io, `Error: Missing required streaming options.\n\nRun: insight ${commandName} --help`);
873
+ io.exit(1);
874
+ return;
875
+ }
876
+ const resolvedValidationError = validateStreamArgs(resolvedArgs, fullSchema);
877
+ if (resolvedValidationError) {
878
+ writeStreamError(io, `Invalid ${toolPromptLabel(resolvedValidationError.key)}: ${resolvedValidationError.error}`);
879
+ io.exit(1);
880
+ return;
881
+ }
882
+ const outputDependencyError = validateStreamOutputDependencies(resolvedArgs, fullSchema);
883
+ if (outputDependencyError) {
884
+ writeStreamError(io, `Error: ${outputDependencyError}`);
885
+ io.exit(1);
886
+ return;
887
+ }
888
+ const apiKey = sessionApiKey?.trim() || io.getApiKey?.()?.trim() || resolveApiKey();
889
+ if (!apiKey) {
890
+ writeStreamError(io, "Error: No API key found.\n\nSet it with: insight login --key <your-api-key>\nOr export: export INSIGHTSENTRY_API_KEY=your-api-key\n\nGet your API key from https://insightsentry.com/dashboard");
891
+ io.exit(1);
892
+ return;
893
+ }
894
+ try {
895
+ const knownArgs = pickKnownArgs(resolvedArgs, Object.keys(fullSchema));
896
+ const coerced = coerceArgs(knownArgs, fullSchema);
897
+ const request = commandName === STREAM_LIVE_COMMAND
898
+ ? buildLiveStreamRequest(pickStreamInput(coerced, LIVE_STREAM_SCHEMA), apiKey)
899
+ : buildNewsfeedStreamRequest(pickStreamInput(coerced, NEWSFEED_STREAM_SCHEMA), apiKey);
900
+ reportArgUsage(io, knownArgs, Object.keys(fullSchema), inputArgs);
901
+ if (request.ignoredArgs.length > 0) {
902
+ io.progress?.(`Ignoring inapplicable subscription args: ${request.ignoredArgs.join(", ")}`);
903
+ }
904
+ const outputFactory = io.createStreamOutput ?? createStreamOutput;
905
+ const streamOutput = await outputFactory({
906
+ outputFile: optionalString(coerced.output_file),
907
+ append: coerced.append === true,
908
+ tee: coerced.tee === true,
909
+ });
910
+ try {
911
+ const runner = io.runStream ?? runWebSocketStream;
912
+ const result = await runner({
913
+ request,
914
+ output: streamOutput,
915
+ diagnostics: io.progress,
916
+ signal: io.streamSignal,
917
+ maxReconnectAttempts: typeof coerced.max_reconnect_attempts === "number"
918
+ ? coerced.max_reconnect_attempts
919
+ : undefined,
920
+ durationMs: typeof coerced.duration_seconds === "number"
921
+ ? coerced.duration_seconds * 1_000
922
+ : undefined,
923
+ maxMessages: typeof coerced.max_messages === "number" ? coerced.max_messages : undefined,
924
+ });
925
+ io.exit(io.getStreamSignalExitCode?.() ?? result.exitCode);
926
+ }
927
+ catch (error) {
928
+ await streamOutput.close().catch(() => { });
929
+ throw error;
930
+ }
931
+ }
932
+ catch (error) {
933
+ writeStreamError(io, `Error: ${errorMessage(error)}`);
934
+ io.exit(1);
935
+ }
936
+ }
937
+ async function resolveStreamArgs(commandName, args, io) {
938
+ if (io.isInteractive !== true || !io.prompt)
939
+ return args;
940
+ const resolved = { ...args };
941
+ if (commandName === STREAM_LIVE_COMMAND) {
942
+ const symbols = await promptForToolArg("symbols", LIVE_STREAM_SCHEMA.symbols, io);
943
+ if (!symbols)
944
+ return null;
945
+ resolved.symbols = symbols;
946
+ const type = await promptForToolArg("type", LIVE_STREAM_SCHEMA.type, io);
947
+ if (!type)
948
+ return null;
949
+ resolved.type = type;
950
+ if (type === "series" || type === "both") {
951
+ const seriesSettings = await promptForStreamChoice("Series Settings", "Use server defaults or configure bar settings.", ["default", "custom"], "default", io);
952
+ if (!seriesSettings)
953
+ return null;
954
+ if (seriesSettings === "custom") {
955
+ const barType = await promptForToolArg("bar_type", LIVE_STREAM_SCHEMA.bar_type, io);
956
+ if (!barType)
957
+ return null;
958
+ resolved.bar_type = barType;
959
+ for (const key of ["bar_interval", "dp"]) {
960
+ const answer = await promptForOptionalToolArg(key, LIVE_STREAM_SCHEMA[key], io);
961
+ if (answer === null)
962
+ return null;
963
+ if (answer !== undefined)
964
+ resolved[key] = answer;
965
+ }
966
+ }
967
+ }
968
+ const advanced = await promptForStreamChoice("Advanced Settings", "Configure session, adjustment, currency, unit, or delivery options.", ["no", "yes"], "no", io);
969
+ if (!advanced)
970
+ return null;
971
+ if (advanced === "yes") {
972
+ for (const key of [
973
+ "currency",
974
+ "unit",
975
+ "extended",
976
+ "24h",
977
+ "split",
978
+ "dadj",
979
+ "badj",
980
+ "settlement",
981
+ "slow_consumer_policy",
982
+ ]) {
983
+ if (shouldSkipLiveStreamPrompt(key, type))
984
+ continue;
985
+ const answer = await promptForOptionalToolArg(key, LIVE_STREAM_SCHEMA[key], io);
986
+ if (answer === null)
987
+ return null;
988
+ if (answer !== undefined)
989
+ resolved[key] = answer;
990
+ }
991
+ }
992
+ }
993
+ else {
994
+ const selection = await promptForStreamChoice("News Selection", "Stream all news or configure matching filters.", ["all", "filtered"], "all", io);
995
+ if (!selection)
996
+ return null;
997
+ if (selection === "filtered") {
998
+ for (const [key, zodType] of Object.entries(NEWSFEED_STREAM_SCHEMA)) {
999
+ const answer = await promptForOptionalToolArg(key, zodType, io);
1000
+ if (answer === null)
1001
+ return null;
1002
+ if (answer !== undefined)
1003
+ resolved[key] = answer;
1004
+ }
1005
+ if (!Object.keys(NEWSFEED_STREAM_SCHEMA).some((key) => hasArgValue(resolved[key]))) {
1006
+ writeStreamError(io, "No newsfeed filters were entered; streaming all news.");
1007
+ }
1008
+ }
1009
+ }
1010
+ return (await resolveInteractiveStreamOutput(resolved, io)) ? resolved : null;
1011
+ }
1012
+ async function resolveInteractiveStreamOutput(resolved, io) {
1013
+ const destination = await promptForStreamChoice("Output Destination", "Choose where received NDJSON messages are written.", ["terminal", "file", "both"], "terminal", io);
1014
+ if (!destination)
1015
+ return false;
1016
+ if (destination === "terminal")
1017
+ return true;
1018
+ const outputFile = await promptForToolArg("output_file", STREAM_OUTPUT_SCHEMA.output_file, io);
1019
+ if (!outputFile)
1020
+ return false;
1021
+ resolved.output_file = outputFile;
1022
+ if (destination === "both")
1023
+ resolved.tee = "true";
1024
+ if (await pathExists(outputFile)) {
1025
+ const existingFileAction = await promptForStreamChoice("Existing Output File", "Append to the existing file or replace it. Press Ctrl+C to cancel.", ["append", "replace"], "append", io);
1026
+ if (!existingFileAction)
1027
+ return false;
1028
+ resolved.append = existingFileAction === "append" ? "true" : "false";
1029
+ }
1030
+ return true;
1031
+ }
1032
+ async function promptForStreamChoice(label, description, choices, defaultValue, io) {
1033
+ const question = `${label}: ${description}\n${label} (choices: ${choices.join("/")}, Default: ${defaultValue}): `;
1034
+ for (let attempt = 0; attempt < MAX_INTERACTIVE_PROMPT_ATTEMPTS; attempt++) {
1035
+ const raw = (await io.prompt?.(question))?.trim().toLowerCase() ?? "";
1036
+ const answer = normalizeStreamChoice(raw || defaultValue, choices);
1037
+ if (answer)
1038
+ return answer;
1039
+ writeStreamError(io, `Invalid ${label}: expected one of: ${choices.join(", ")}`);
1040
+ }
1041
+ return null;
1042
+ }
1043
+ function normalizeStreamChoice(value, choices) {
1044
+ const aliases = {
1045
+ y: "yes",
1046
+ n: "no",
1047
+ stdout: "terminal",
1048
+ tee: "both",
1049
+ };
1050
+ const normalized = aliases[value] ?? value;
1051
+ return choices.includes(normalized) ? normalized : null;
1052
+ }
1053
+ async function pathExists(value) {
1054
+ try {
1055
+ await access(value);
1056
+ return true;
1057
+ }
1058
+ catch {
1059
+ return false;
1060
+ }
1061
+ }
1062
+ function validateStreamArgs(args, schema) {
1063
+ for (const [key, zodType] of Object.entries(schema)) {
1064
+ if (args[key] === undefined)
1065
+ continue;
1066
+ const error = validateToolArgAnswer(key, zodType, args[key]);
1067
+ if (error)
1068
+ return { key, error };
1069
+ }
1070
+ return null;
1071
+ }
1072
+ function collectInvalidStreamArgs(args, schema, valuelessArgs) {
1073
+ const valueless = new Set(valuelessArgs);
1074
+ const errors = [];
1075
+ for (const [key, zodType] of Object.entries(schema)) {
1076
+ if (args[key] === undefined)
1077
+ continue;
1078
+ if (valueless.has(key) && getZodTypeName(zodType) !== "boolean") {
1079
+ errors.push({ key, error: "value is required" });
1080
+ continue;
1081
+ }
1082
+ const error = validateToolArgAnswer(key, zodType, args[key]);
1083
+ if (error)
1084
+ errors.push({ key, error });
1085
+ }
1086
+ return errors;
1087
+ }
1088
+ function validateStreamOutputDependencies(args, schema) {
1089
+ if (hasArgValue(args.output_file))
1090
+ return null;
1091
+ const coerced = coerceArgs(args, schema);
1092
+ if (coerced.append === true)
1093
+ return "--append requires --output_file.";
1094
+ if (coerced.tee === true)
1095
+ return "--tee requires --output_file.";
1096
+ return null;
1097
+ }
1098
+ function writeStreamError(io, message) {
1099
+ (io.progress ?? io.write)(message);
1100
+ }
1101
+ function closestStreamOption(input, options) {
1102
+ const boundedInput = input.slice(0, 64);
1103
+ let closest;
1104
+ for (const option of options) {
1105
+ const distance = editDistance(boundedInput, option);
1106
+ if (!closest || distance < closest.distance)
1107
+ closest = { option, distance };
1108
+ }
1109
+ if (!closest || closest.distance > Math.max(2, Math.floor(boundedInput.length / 3)))
1110
+ return null;
1111
+ return closest.option;
1112
+ }
1113
+ function editDistance(left, right) {
1114
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
1115
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex++) {
1116
+ const current = [leftIndex];
1117
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex++) {
1118
+ current[rightIndex] = Math.min(current[rightIndex - 1] + 1, previous[rightIndex] + 1, previous[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1));
1119
+ }
1120
+ previous = current;
1121
+ }
1122
+ return previous[right.length];
1123
+ }
1124
+ function pickStreamInput(values, schema) {
1125
+ const known = new Set(Object.keys(schema));
1126
+ return Object.fromEntries(Object.entries(values).filter(([key]) => known.has(key)));
1127
+ }
1128
+ function shouldSkipLiveStreamPrompt(key, type) {
1129
+ if (type !== "quote" && type !== "quotes")
1130
+ return false;
1131
+ return [
1132
+ "bar_type",
1133
+ "bar_interval",
1134
+ "dp",
1135
+ "currency",
1136
+ "unit",
1137
+ "split",
1138
+ "dadj",
1139
+ "badj",
1140
+ "settlement",
1141
+ ].includes(key);
1142
+ }
1143
+ function optionalString(value) {
1144
+ return typeof value === "string" && value.trim() ? value : undefined;
1145
+ }
1146
+ function errorMessage(error) {
1147
+ return error instanceof Error ? error.message : String(error);
1148
+ }
704
1149
  async function runUpdateCommand(io) {
705
1150
  const runCommand = io.runCommand ?? defaultRunCommand;
706
1151
  const status = await getCachedVersionStatus(io);
@@ -767,11 +1212,22 @@ function interactiveToolOptions() {
767
1212
  name: RENDER_CHART_COMMAND,
768
1213
  description: "Render Chart.js configs as PNG images",
769
1214
  },
1215
+ {
1216
+ name: STREAM_LIVE_COMMAND,
1217
+ description: "Stream real-time market data as NDJSON",
1218
+ },
1219
+ {
1220
+ name: STREAM_NEWSFEED_COMMAND,
1221
+ description: "Stream financial news as NDJSON",
1222
+ },
770
1223
  ];
771
1224
  }
772
1225
  function isHistoryDownloadCommand(toolName) {
773
1226
  return toolName === DOWNLOAD_HISTORY_COMMAND || toolName === GET_SYMBOL_HISTORY_COMMAND;
774
1227
  }
1228
+ function isStreamCommand(toolName) {
1229
+ return toolName === STREAM_LIVE_COMMAND || toolName === STREAM_NEWSFEED_COMMAND;
1230
+ }
775
1231
  function buildInteractiveToolList(options) {
776
1232
  return [
777
1233
  "Choose a tool:",
@@ -1596,6 +2052,21 @@ function selectToolWithKeyboard(options) {
1596
2052
  export function main() {
1597
2053
  const rl = createInterface({ input, output });
1598
2054
  const interactive = process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true";
2055
+ const streamAbort = new AbortController();
2056
+ let streamRunning = false;
2057
+ let signalExitCode;
2058
+ const handleSignal = (exitCode) => {
2059
+ signalExitCode = exitCode;
2060
+ streamAbort.abort();
2061
+ if (!streamRunning) {
2062
+ process.exitCode = exitCode;
2063
+ rl.close();
2064
+ }
2065
+ };
2066
+ const handleSigint = () => handleSignal(130);
2067
+ const handleSigterm = () => handleSignal(143);
2068
+ process.once("SIGINT", handleSigint);
2069
+ process.once("SIGTERM", handleSigterm);
1599
2070
  runCli(process.argv.slice(2), {
1600
2071
  write: (s) => process.stdout.write(`${s}\n`),
1601
2072
  progress: (s) => process.stderr.write(`${s}\n`),
@@ -1603,7 +2074,29 @@ export function main() {
1603
2074
  prompt: (question) => rl.question(question),
1604
2075
  selectTool: canUseKeyboardToolSelection() ? selectToolWithKeyboard : undefined,
1605
2076
  isInteractive: interactive,
1606
- exit: (code) => process.exit(code),
1607
- }).finally(() => rl.close());
2077
+ streamSignal: streamAbort.signal,
2078
+ getStreamSignalExitCode: () => signalExitCode,
2079
+ runStream: async (options) => {
2080
+ streamRunning = true;
2081
+ try {
2082
+ return await runWebSocketStream(options);
2083
+ }
2084
+ finally {
2085
+ streamRunning = false;
2086
+ }
2087
+ },
2088
+ exit: (code) => {
2089
+ process.exitCode = code;
2090
+ },
2091
+ })
2092
+ .catch((error) => {
2093
+ process.stderr.write(`Error: ${errorMessage(error)}\n`);
2094
+ process.exitCode = 1;
2095
+ })
2096
+ .finally(() => {
2097
+ process.removeListener("SIGINT", handleSigint);
2098
+ process.removeListener("SIGTERM", handleSigterm);
2099
+ rl.close();
2100
+ });
1608
2101
  }
1609
2102
  //# sourceMappingURL=cli.js.map