@insightsentry/mcp 1.4.39 → 1.4.41

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,21 @@ 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 { LIVE_STREAM_SCHEMA, NEWSFEED_STREAM_SCHEMA, STREAM_OUTPUT_SCHEMA, } from "./websocket-schemas.js";
26
+ import { runWebSocketStream, } from "./websocket-stream.js";
22
27
  export { coerceArgs } from "./arg-coercion.js";
23
28
  const DOWNLOAD_HISTORY_COMMAND = "download_history";
24
29
  const GET_SYMBOL_HISTORY_COMMAND = "get_symbol_history";
25
30
  const RENDER_CHART_COMMAND = "render_chart";
31
+ const STREAM_LIVE_COMMAND = "stream_live";
32
+ const STREAM_NEWSFEED_COMMAND = "stream_newsfeed";
26
33
  const UPDATE_COMMAND = "update";
27
34
  const VERSION_COMMAND = "version";
28
35
  const MAX_INTERACTIVE_PROMPT_ATTEMPTS = 3;
@@ -71,6 +78,7 @@ const RENDER_CHART_SCHEMA = {
71
78
  };
72
79
  export function parseArgs(argv) {
73
80
  const args = {};
81
+ const valuelessArgs = new Set();
74
82
  let toolName = null;
75
83
  let help = false;
76
84
  let version = false;
@@ -97,10 +105,12 @@ export function parseArgs(argv) {
97
105
  const next = argv[i + 1];
98
106
  if (next === undefined || next.startsWith("--")) {
99
107
  args[key] = "true";
108
+ valuelessArgs.add(key);
100
109
  i++;
101
110
  }
102
111
  else {
103
112
  args[key] = next;
113
+ valuelessArgs.delete(key);
104
114
  i += 2;
105
115
  }
106
116
  }
@@ -108,7 +118,7 @@ export function parseArgs(argv) {
108
118
  i++;
109
119
  }
110
120
  }
111
- return { toolName, args, help, version };
121
+ return { toolName, args, valuelessArgs: [...valuelessArgs], help, version };
112
122
  }
113
123
  export function buildVersion() {
114
124
  return `${PACKAGE_JSON.name} ${PACKAGE_JSON.version}`;
@@ -130,6 +140,8 @@ export function buildHelp() {
130
140
  lines.push(` ${DOWNLOAD_HISTORY_COMMAND.padEnd(32)} Download historical ranges to JSON/CSV files with concurrency and progress`);
131
141
  lines.push(` ${GET_SYMBOL_HISTORY_COMMAND.padEnd(32)} Same as download_history; downloads history to files`);
132
142
  lines.push(` ${RENDER_CHART_COMMAND.padEnd(32)} Render Chart.js configs as PNG images`);
143
+ lines.push(` ${STREAM_LIVE_COMMAND.padEnd(32)} Stream real-time market data as NDJSON`);
144
+ lines.push(` ${STREAM_NEWSFEED_COMMAND.padEnd(32)} Stream financial news as NDJSON`);
133
145
  lines.push("");
134
146
  lines.push("Quick Start:");
135
147
  lines.push(' insight search_symbols --query "apple"');
@@ -139,6 +151,8 @@ export function buildHelp() {
139
151
  lines.push(' insight screen_stocks --fields "close,volume,market_cap" --exchanges "NYSE,NASDAQ" --sortBy market_cap --sortOrder desc');
140
152
  lines.push(" insight get_earnings --c US");
141
153
  lines.push(' insight get_options_contracts --code "NASDAQ:AAPL" --type call --range 10');
154
+ lines.push(' insight stream_live --symbols "NASDAQ:AAPL" --type both --bar_type minute --max_messages 100 --duration_seconds 30');
155
+ lines.push(" insight stream_newsfeed --output_file ./news.ndjson --append --duration_seconds 30");
142
156
  lines.push("");
143
157
  lines.push("API tools support --filter <jsonata> to transform the response.");
144
158
  lines.push("Use: insight <tool> --help for tool-specific parameters.");
@@ -201,6 +215,54 @@ export function buildRenderChartHelp() {
201
215
  ' insight render_chart --config \'{"type":"bar","data":{"labels":["Jan"],"datasets":[{"label":"Volume","data":[1200]}]}}\' --width 1200 --height 600',
202
216
  ].join("\n");
203
217
  }
218
+ export function buildStreamHelp(commandName) {
219
+ const isLive = commandName === STREAM_LIVE_COMMAND;
220
+ const schema = isLive
221
+ ? { ...LIVE_STREAM_SCHEMA, ...STREAM_OUTPUT_SCHEMA }
222
+ : { ...NEWSFEED_STREAM_SCHEMA, ...STREAM_OUTPUT_SCHEMA };
223
+ const lines = [
224
+ `insight ${commandName}`,
225
+ "",
226
+ isLive
227
+ ? "Stream real-time quotes and series as newline-delimited JSON. Use --symbols with --type for common settings, or --subscriptions for per-symbol settings."
228
+ : "Stream matching financial news as newline-delimited JSON.",
229
+ ];
230
+ if (isLive) {
231
+ lines.push("", "Subscription modes:", " Simple: --symbols <codes> --type <quote|series|both>", " Per-symbol: --subscriptions <json-array>");
232
+ }
233
+ lines.push("", "Parameters:");
234
+ for (const [key, zodType] of Object.entries(schema)) {
235
+ const requirement = isLive && key === "symbols"
236
+ ? "[required with --type]"
237
+ : isLive && key === "type"
238
+ ? "[required with --symbols]"
239
+ : isLive && key === "subscriptions"
240
+ ? "[alternative to --symbols/--type]"
241
+ : "[optional]";
242
+ lines.push(` --${key.padEnd(24)} ${formatTypeName(zodType)} ${requirement} ${formatHelpDescription(zodType)}`);
243
+ }
244
+ 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.", "Use --quiet to suppress WebSocket status diagnostics for machine-readable terminal output.");
245
+ if (isLive) {
246
+ lines.push("Quote updates can be sparse; merge updates by code when maintaining a current snapshot.");
247
+ }
248
+ else {
249
+ 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.");
250
+ }
251
+ lines.push("");
252
+ lines.push("Examples:");
253
+ if (isLive) {
254
+ lines.push(' insight stream_live --symbols "NASDAQ:AAPL,NASDAQ:MSFT" --type both --bar_type minute --bar_interval 1 --max_messages 100 --duration_seconds 30');
255
+ lines.push(` insight stream_live --subscriptions '[{"code":"NASDAQ:AAPL","type":"series","bar_type":"day"},{"code":"NASDAQ:MSFT","type":"quote"}]' --output_file ./market.ndjson --append --duration_seconds 60`);
256
+ }
257
+ else {
258
+ lines.push(" insight stream_newsfeed --max_messages 20 --duration_seconds 30");
259
+ lines.push(' insight stream_newsfeed --content "earnings" --related_symbols "NASDAQ:AAPL" --output_file ./news.ndjson --append --duration_seconds 60');
260
+ }
261
+ lines.push("");
262
+ lines.push("Press Ctrl+C to close the socket after queued output is written.");
263
+ lines.push("Docs: https://insightsentry.com/docs/ws");
264
+ return lines.join("\n");
265
+ }
204
266
  const toolExamples = {
205
267
  search_symbols: [
206
268
  'insight search_symbols --query "tesla"',
@@ -393,7 +455,7 @@ function isPromptAbortError(error) {
393
455
  return candidate.code === "ABORT_ERR" || candidate.name === "AbortError";
394
456
  }
395
457
  async function runCliInner(argv, io) {
396
- let { toolName, args, help, version } = parseArgs(argv);
458
+ let { toolName, args, valuelessArgs, help, version } = parseArgs(argv);
397
459
  let sessionApiKey;
398
460
  if (version) {
399
461
  io.write(buildVersion());
@@ -466,6 +528,15 @@ async function runCliInner(argv, io) {
466
528
  }
467
529
  return;
468
530
  }
531
+ if (isStreamCommand(toolName)) {
532
+ if (help) {
533
+ io.write(buildStreamHelp(toolName));
534
+ io.exit(0);
535
+ return;
536
+ }
537
+ await runStreamCliCommand(toolName, args, valuelessArgs, sessionApiKey, io);
538
+ return;
539
+ }
469
540
  if (isHistoryDownloadCommand(toolName)) {
470
541
  if (help) {
471
542
  io.write(buildDownloadHistoryHelp(toolName));
@@ -701,6 +772,306 @@ async function resolveInteractiveToolName(io) {
701
772
  io.write("No tool selected.");
702
773
  return null;
703
774
  }
775
+ async function runStreamCliCommand(commandName, args, valuelessArgs, sessionApiKey, io) {
776
+ const inputSchema = commandName === STREAM_LIVE_COMMAND ? LIVE_STREAM_SCHEMA : NEWSFEED_STREAM_SCHEMA;
777
+ const fullSchema = { ...inputSchema, ...STREAM_OUTPUT_SCHEMA };
778
+ const inputArgs = { ...args };
779
+ const guided = io.isInteractive === true && Boolean(io.prompt) && Object.keys(args).length === 0;
780
+ const unknownArg = Object.keys(inputArgs).find((key) => !(key in fullSchema));
781
+ if (unknownArg) {
782
+ const suggestion = closestStreamOption(unknownArg, Object.keys(fullSchema));
783
+ writeStreamError(io, `Error: Unknown option --${unknownArg}.${suggestion ? ` Did you mean --${suggestion}?` : ` Run: insight ${commandName} --help`}`);
784
+ io.exit(1);
785
+ return;
786
+ }
787
+ const validationErrors = collectInvalidStreamArgs(inputArgs, fullSchema, valuelessArgs);
788
+ if (validationErrors.length > 0) {
789
+ const validationError = validationErrors[0];
790
+ writeStreamError(io, `Invalid ${toolPromptLabel(validationError.key)}: ${validationError.error}`);
791
+ io.exit(1);
792
+ return;
793
+ }
794
+ const resolvedArgs = guided ? await resolveStreamArgs(commandName, inputArgs, io) : inputArgs;
795
+ if (!resolvedArgs) {
796
+ writeStreamError(io, `Error: Missing required streaming options.\n\nRun: insight ${commandName} --help`);
797
+ io.exit(1);
798
+ return;
799
+ }
800
+ const resolvedValidationError = validateStreamArgs(resolvedArgs, fullSchema);
801
+ if (resolvedValidationError) {
802
+ writeStreamError(io, `Invalid ${toolPromptLabel(resolvedValidationError.key)}: ${resolvedValidationError.error}`);
803
+ io.exit(1);
804
+ return;
805
+ }
806
+ const outputDependencyError = validateStreamOutputDependencies(resolvedArgs, fullSchema);
807
+ if (outputDependencyError) {
808
+ writeStreamError(io, `Error: ${outputDependencyError}`);
809
+ io.exit(1);
810
+ return;
811
+ }
812
+ const apiKey = sessionApiKey?.trim() || io.getApiKey?.()?.trim() || resolveApiKey();
813
+ if (!apiKey) {
814
+ 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");
815
+ io.exit(1);
816
+ return;
817
+ }
818
+ try {
819
+ const knownArgs = pickKnownArgs(resolvedArgs, Object.keys(fullSchema));
820
+ const coerced = coerceArgs(knownArgs, fullSchema);
821
+ const request = commandName === STREAM_LIVE_COMMAND
822
+ ? buildLiveStreamRequest(pickStreamInput(coerced, LIVE_STREAM_SCHEMA), apiKey)
823
+ : buildNewsfeedStreamRequest(pickStreamInput(coerced, NEWSFEED_STREAM_SCHEMA), apiKey);
824
+ if (coerced.quiet !== true) {
825
+ reportArgUsage(io, knownArgs, Object.keys(fullSchema), inputArgs);
826
+ }
827
+ if (coerced.quiet !== true && request.ignoredArgs.length > 0) {
828
+ io.progress?.(`Ignoring inapplicable subscription args: ${request.ignoredArgs.join(", ")}`);
829
+ }
830
+ const outputFactory = io.createStreamOutput ?? createStreamOutput;
831
+ const streamOutput = await outputFactory({
832
+ outputFile: optionalString(coerced.output_file),
833
+ append: coerced.append === true,
834
+ tee: coerced.tee === true,
835
+ });
836
+ try {
837
+ const runner = io.runStream ?? runWebSocketStream;
838
+ const result = await runner({
839
+ request,
840
+ output: streamOutput,
841
+ diagnostics: coerced.quiet === true ? () => { } : io.progress,
842
+ signal: io.streamSignal,
843
+ maxReconnectAttempts: typeof coerced.max_reconnect_attempts === "number"
844
+ ? coerced.max_reconnect_attempts
845
+ : undefined,
846
+ durationMs: typeof coerced.duration_seconds === "number"
847
+ ? coerced.duration_seconds * 1_000
848
+ : undefined,
849
+ maxMessages: typeof coerced.max_messages === "number" ? coerced.max_messages : undefined,
850
+ });
851
+ io.exit(io.getStreamSignalExitCode?.() ?? result.exitCode);
852
+ }
853
+ catch (error) {
854
+ await streamOutput.close().catch(() => { });
855
+ throw error;
856
+ }
857
+ }
858
+ catch (error) {
859
+ writeStreamError(io, `Error: ${errorMessage(error)}`);
860
+ io.exit(1);
861
+ }
862
+ }
863
+ async function resolveStreamArgs(commandName, args, io) {
864
+ if (io.isInteractive !== true || !io.prompt)
865
+ return args;
866
+ const resolved = { ...args };
867
+ if (commandName === STREAM_LIVE_COMMAND) {
868
+ const symbols = await promptForToolArg("symbols", LIVE_STREAM_SCHEMA.symbols, io);
869
+ if (!symbols)
870
+ return null;
871
+ resolved.symbols = symbols;
872
+ const type = await promptForToolArg("type", LIVE_STREAM_SCHEMA.type, io);
873
+ if (!type)
874
+ return null;
875
+ resolved.type = type;
876
+ if (type === "series" || type === "both") {
877
+ const seriesSettings = await promptForStreamChoice("Series Settings", "Use server defaults or configure bar settings.", ["default", "custom"], "default", io);
878
+ if (!seriesSettings)
879
+ return null;
880
+ if (seriesSettings === "custom") {
881
+ const barType = await promptForToolArg("bar_type", LIVE_STREAM_SCHEMA.bar_type, io);
882
+ if (!barType)
883
+ return null;
884
+ resolved.bar_type = barType;
885
+ for (const key of ["bar_interval", "dp"]) {
886
+ const answer = await promptForOptionalToolArg(key, LIVE_STREAM_SCHEMA[key], io);
887
+ if (answer === null)
888
+ return null;
889
+ if (answer !== undefined)
890
+ resolved[key] = answer;
891
+ }
892
+ }
893
+ }
894
+ const advanced = await promptForStreamChoice("Advanced Settings", "Configure session, adjustment, currency, unit, or delivery options.", ["no", "yes"], "no", io);
895
+ if (!advanced)
896
+ return null;
897
+ if (advanced === "yes") {
898
+ for (const key of [
899
+ "currency",
900
+ "unit",
901
+ "extended",
902
+ "24h",
903
+ "split",
904
+ "dadj",
905
+ "badj",
906
+ "settlement",
907
+ "slow_consumer_policy",
908
+ ]) {
909
+ if (shouldSkipLiveStreamPrompt(key, type))
910
+ continue;
911
+ const answer = await promptForOptionalToolArg(key, LIVE_STREAM_SCHEMA[key], io);
912
+ if (answer === null)
913
+ return null;
914
+ if (answer !== undefined)
915
+ resolved[key] = answer;
916
+ }
917
+ }
918
+ }
919
+ else {
920
+ const selection = await promptForStreamChoice("News Selection", "Stream all news or configure matching filters.", ["all", "filtered"], "all", io);
921
+ if (!selection)
922
+ return null;
923
+ if (selection === "filtered") {
924
+ for (const [key, zodType] of Object.entries(NEWSFEED_STREAM_SCHEMA)) {
925
+ const answer = await promptForOptionalToolArg(key, zodType, io);
926
+ if (answer === null)
927
+ return null;
928
+ if (answer !== undefined)
929
+ resolved[key] = answer;
930
+ }
931
+ if (!Object.keys(NEWSFEED_STREAM_SCHEMA).some((key) => hasArgValue(resolved[key]))) {
932
+ writeStreamError(io, "No newsfeed filters were entered; streaming all news.");
933
+ }
934
+ }
935
+ }
936
+ return (await resolveInteractiveStreamOutput(resolved, io)) ? resolved : null;
937
+ }
938
+ async function resolveInteractiveStreamOutput(resolved, io) {
939
+ const destination = await promptForStreamChoice("Output Destination", "Choose where received NDJSON messages are written.", ["terminal", "file", "both"], "terminal", io);
940
+ if (!destination)
941
+ return false;
942
+ if (destination === "terminal")
943
+ return true;
944
+ const outputFile = await promptForToolArg("output_file", STREAM_OUTPUT_SCHEMA.output_file, io);
945
+ if (!outputFile)
946
+ return false;
947
+ resolved.output_file = outputFile;
948
+ if (destination === "both")
949
+ resolved.tee = "true";
950
+ if (await pathExists(outputFile)) {
951
+ const existingFileAction = await promptForStreamChoice("Existing Output File", "Append to the existing file or replace it. Press Ctrl+C to cancel.", ["append", "replace"], "append", io);
952
+ if (!existingFileAction)
953
+ return false;
954
+ resolved.append = existingFileAction === "append" ? "true" : "false";
955
+ }
956
+ return true;
957
+ }
958
+ async function promptForStreamChoice(label, description, choices, defaultValue, io) {
959
+ const question = `${label}: ${description}\n${label} (choices: ${choices.join("/")}, Default: ${defaultValue}): `;
960
+ for (let attempt = 0; attempt < MAX_INTERACTIVE_PROMPT_ATTEMPTS; attempt++) {
961
+ const raw = (await io.prompt?.(question))?.trim().toLowerCase() ?? "";
962
+ const answer = normalizeStreamChoice(raw || defaultValue, choices);
963
+ if (answer)
964
+ return answer;
965
+ writeStreamError(io, `Invalid ${label}: expected one of: ${choices.join(", ")}`);
966
+ }
967
+ return null;
968
+ }
969
+ function normalizeStreamChoice(value, choices) {
970
+ const aliases = {
971
+ y: "yes",
972
+ n: "no",
973
+ stdout: "terminal",
974
+ tee: "both",
975
+ };
976
+ const normalized = aliases[value] ?? value;
977
+ return choices.includes(normalized) ? normalized : null;
978
+ }
979
+ async function pathExists(value) {
980
+ try {
981
+ await access(value);
982
+ return true;
983
+ }
984
+ catch {
985
+ return false;
986
+ }
987
+ }
988
+ function validateStreamArgs(args, schema) {
989
+ for (const [key, zodType] of Object.entries(schema)) {
990
+ if (args[key] === undefined)
991
+ continue;
992
+ const error = validateToolArgAnswer(key, zodType, args[key]);
993
+ if (error)
994
+ return { key, error };
995
+ }
996
+ return null;
997
+ }
998
+ function collectInvalidStreamArgs(args, schema, valuelessArgs) {
999
+ const valueless = new Set(valuelessArgs);
1000
+ const errors = [];
1001
+ for (const [key, zodType] of Object.entries(schema)) {
1002
+ if (args[key] === undefined)
1003
+ continue;
1004
+ if (valueless.has(key) && getZodTypeName(zodType) !== "boolean") {
1005
+ errors.push({ key, error: "value is required" });
1006
+ continue;
1007
+ }
1008
+ const error = validateToolArgAnswer(key, zodType, args[key]);
1009
+ if (error)
1010
+ errors.push({ key, error });
1011
+ }
1012
+ return errors;
1013
+ }
1014
+ function validateStreamOutputDependencies(args, schema) {
1015
+ if (hasArgValue(args.output_file))
1016
+ return null;
1017
+ const coerced = coerceArgs(args, schema);
1018
+ if (coerced.append === true)
1019
+ return "--append requires --output_file.";
1020
+ if (coerced.tee === true)
1021
+ return "--tee requires --output_file.";
1022
+ return null;
1023
+ }
1024
+ function writeStreamError(io, message) {
1025
+ (io.progress ?? io.write)(message);
1026
+ }
1027
+ function closestStreamOption(input, options) {
1028
+ const boundedInput = input.slice(0, 64);
1029
+ let closest;
1030
+ for (const option of options) {
1031
+ const distance = editDistance(boundedInput, option);
1032
+ if (!closest || distance < closest.distance)
1033
+ closest = { option, distance };
1034
+ }
1035
+ if (!closest || closest.distance > Math.max(2, Math.floor(boundedInput.length / 3)))
1036
+ return null;
1037
+ return closest.option;
1038
+ }
1039
+ function editDistance(left, right) {
1040
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
1041
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex++) {
1042
+ const current = [leftIndex];
1043
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex++) {
1044
+ current[rightIndex] = Math.min(current[rightIndex - 1] + 1, previous[rightIndex] + 1, previous[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1));
1045
+ }
1046
+ previous = current;
1047
+ }
1048
+ return previous[right.length];
1049
+ }
1050
+ function pickStreamInput(values, schema) {
1051
+ const known = new Set(Object.keys(schema));
1052
+ return Object.fromEntries(Object.entries(values).filter(([key]) => known.has(key)));
1053
+ }
1054
+ function shouldSkipLiveStreamPrompt(key, type) {
1055
+ if (type !== "quote" && type !== "quotes")
1056
+ return false;
1057
+ return [
1058
+ "bar_type",
1059
+ "bar_interval",
1060
+ "dp",
1061
+ "currency",
1062
+ "unit",
1063
+ "split",
1064
+ "dadj",
1065
+ "badj",
1066
+ "settlement",
1067
+ ].includes(key);
1068
+ }
1069
+ function optionalString(value) {
1070
+ return typeof value === "string" && value.trim() ? value : undefined;
1071
+ }
1072
+ function errorMessage(error) {
1073
+ return error instanceof Error ? error.message : String(error);
1074
+ }
704
1075
  async function runUpdateCommand(io) {
705
1076
  const runCommand = io.runCommand ?? defaultRunCommand;
706
1077
  const status = await getCachedVersionStatus(io);
@@ -767,11 +1138,22 @@ function interactiveToolOptions() {
767
1138
  name: RENDER_CHART_COMMAND,
768
1139
  description: "Render Chart.js configs as PNG images",
769
1140
  },
1141
+ {
1142
+ name: STREAM_LIVE_COMMAND,
1143
+ description: "Stream real-time market data as NDJSON",
1144
+ },
1145
+ {
1146
+ name: STREAM_NEWSFEED_COMMAND,
1147
+ description: "Stream financial news as NDJSON",
1148
+ },
770
1149
  ];
771
1150
  }
772
1151
  function isHistoryDownloadCommand(toolName) {
773
1152
  return toolName === DOWNLOAD_HISTORY_COMMAND || toolName === GET_SYMBOL_HISTORY_COMMAND;
774
1153
  }
1154
+ function isStreamCommand(toolName) {
1155
+ return toolName === STREAM_LIVE_COMMAND || toolName === STREAM_NEWSFEED_COMMAND;
1156
+ }
775
1157
  function buildInteractiveToolList(options) {
776
1158
  return [
777
1159
  "Choose a tool:",
@@ -1596,6 +1978,21 @@ function selectToolWithKeyboard(options) {
1596
1978
  export function main() {
1597
1979
  const rl = createInterface({ input, output });
1598
1980
  const interactive = process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true";
1981
+ const streamAbort = new AbortController();
1982
+ let streamRunning = false;
1983
+ let signalExitCode;
1984
+ const handleSignal = (exitCode) => {
1985
+ signalExitCode = exitCode;
1986
+ streamAbort.abort();
1987
+ if (!streamRunning) {
1988
+ process.exitCode = exitCode;
1989
+ rl.close();
1990
+ }
1991
+ };
1992
+ const handleSigint = () => handleSignal(130);
1993
+ const handleSigterm = () => handleSignal(143);
1994
+ process.once("SIGINT", handleSigint);
1995
+ process.once("SIGTERM", handleSigterm);
1599
1996
  runCli(process.argv.slice(2), {
1600
1997
  write: (s) => process.stdout.write(`${s}\n`),
1601
1998
  progress: (s) => process.stderr.write(`${s}\n`),
@@ -1603,7 +2000,29 @@ export function main() {
1603
2000
  prompt: (question) => rl.question(question),
1604
2001
  selectTool: canUseKeyboardToolSelection() ? selectToolWithKeyboard : undefined,
1605
2002
  isInteractive: interactive,
1606
- exit: (code) => process.exit(code),
1607
- }).finally(() => rl.close());
2003
+ streamSignal: streamAbort.signal,
2004
+ getStreamSignalExitCode: () => signalExitCode,
2005
+ runStream: async (options) => {
2006
+ streamRunning = true;
2007
+ try {
2008
+ return await runWebSocketStream(options);
2009
+ }
2010
+ finally {
2011
+ streamRunning = false;
2012
+ }
2013
+ },
2014
+ exit: (code) => {
2015
+ process.exitCode = code;
2016
+ },
2017
+ })
2018
+ .catch((error) => {
2019
+ process.stderr.write(`Error: ${errorMessage(error)}\n`);
2020
+ process.exitCode = 1;
2021
+ })
2022
+ .finally(() => {
2023
+ process.removeListener("SIGINT", handleSigint);
2024
+ process.removeListener("SIGTERM", handleSigterm);
2025
+ rl.close();
2026
+ });
1608
2027
  }
1609
2028
  //# sourceMappingURL=cli.js.map