@juspay/neurolink 10.0.0 → 10.1.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.
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console, curly */
3
2
  /**
4
3
  * NeuroLink CLI Observability Commands
5
4
  *
@@ -14,6 +13,7 @@ import ora from "ora";
14
13
  import { logger } from "../../lib/utils/logger.js";
15
14
  import { NeuroLink } from "../../lib/neurolink.js";
16
15
  import { formatRow, formatCost } from "../utils/formatters.js";
16
+ import { redactUrlCredentials } from "../../lib/utils/logSanitize.js";
17
17
  /**
18
18
  * Observability Command Factory
19
19
  */
@@ -70,67 +70,72 @@ export class ObservabilityCommandFactory {
70
70
  try {
71
71
  const neurolink = new NeuroLink();
72
72
  const status = neurolink.getTelemetryStatus();
73
- if (spinner)
73
+ if (spinner) {
74
74
  spinner.succeed("Status retrieved");
75
+ }
75
76
  if (args.format === "json") {
76
- console.log(JSON.stringify(status, null, 2));
77
+ logger.always(JSON.stringify(status, (_key, value) => typeof value === "string" &&
78
+ (_key === "baseUrl" || _key === "endpoint")
79
+ ? redactUrlCredentials(value)
80
+ : value, 2));
77
81
  }
78
82
  else {
79
- console.log("");
80
- console.log(chalk.bold.cyan("=== Observability Status ==="));
81
- console.log("");
83
+ logger.always("");
84
+ logger.always(chalk.bold.cyan("=== Observability Status ==="));
85
+ logger.always("");
82
86
  // Telemetry enabled status
83
87
  const enabledIcon = status.enabled
84
88
  ? chalk.green("ON")
85
89
  : chalk.red("OFF");
86
- console.log(formatRow("Telemetry:", enabledIcon));
90
+ logger.always(formatRow("Telemetry:", enabledIcon));
87
91
  // Langfuse status
88
92
  if (status.langfuse) {
89
- console.log("");
90
- console.log(chalk.bold("Langfuse:"));
93
+ logger.always("");
94
+ logger.always(chalk.bold("Langfuse:"));
91
95
  const lfStatus = status.langfuse.enabled
92
96
  ? chalk.green("Enabled")
93
97
  : chalk.gray("Disabled");
94
- console.log(formatRow(" Status:", lfStatus));
98
+ logger.always(formatRow(" Status:", lfStatus));
95
99
  if (status.langfuse.baseUrl) {
96
- console.log(formatRow(" URL:", status.langfuse.baseUrl));
100
+ logger.always(formatRow(" URL:", redactUrlCredentials(status.langfuse.baseUrl)));
97
101
  }
98
102
  if (status.langfuse.environment) {
99
- console.log(formatRow(" Environment:", status.langfuse.environment));
103
+ logger.always(formatRow(" Environment:", status.langfuse.environment));
100
104
  }
101
105
  }
102
106
  // OpenTelemetry status
103
107
  if (status.openTelemetry) {
104
- console.log("");
105
- console.log(chalk.bold("OpenTelemetry:"));
108
+ logger.always("");
109
+ logger.always(chalk.bold("OpenTelemetry:"));
106
110
  const otelStatus = status.openTelemetry.enabled
107
111
  ? chalk.green("Enabled")
108
112
  : chalk.gray("Disabled");
109
- console.log(formatRow(" Status:", otelStatus));
113
+ logger.always(formatRow(" Status:", otelStatus));
110
114
  if (status.openTelemetry.endpoint) {
111
- console.log(formatRow(" Endpoint:", status.openTelemetry.endpoint));
115
+ logger.always(formatRow(" Endpoint:", redactUrlCredentials(status.openTelemetry.endpoint)));
112
116
  }
113
117
  if (status.openTelemetry.serviceName) {
114
- console.log(formatRow(" Service:", status.openTelemetry.serviceName));
118
+ logger.always(formatRow(" Service:", status.openTelemetry.serviceName));
115
119
  }
116
120
  }
117
121
  // Exporters summary
118
122
  if (status.exporters && status.exporters.length > 0) {
119
- console.log("");
120
- console.log(chalk.bold("Active Exporters:"));
123
+ logger.always("");
124
+ logger.always(chalk.bold("Active Exporters:"));
121
125
  for (const exporter of status.exporters) {
122
126
  const exporterStatus = exporter.healthy
123
127
  ? chalk.green("Healthy")
124
128
  : chalk.red("Unhealthy");
125
- console.log(formatRow(` ${exporter.name}:`, exporterStatus));
129
+ logger.always(formatRow(` ${exporter.name}:`, exporterStatus));
126
130
  }
127
131
  }
128
- console.log("");
132
+ logger.always("");
129
133
  }
130
134
  }
131
135
  catch (error) {
132
- if (spinner)
136
+ if (spinner) {
133
137
  spinner.fail("Failed to get status");
138
+ }
134
139
  logger.error("Error:", error instanceof Error ? error.message : String(error));
135
140
  process.exit(1);
136
141
  }
@@ -171,74 +176,76 @@ export class ObservabilityCommandFactory {
171
176
  try {
172
177
  const neurolink = new NeuroLink();
173
178
  const metrics = neurolink.getMetrics();
174
- if (spinner)
179
+ if (spinner) {
175
180
  spinner.succeed("Metrics retrieved");
181
+ }
176
182
  if (args.format === "json") {
177
- console.log(JSON.stringify(metrics, null, 2));
183
+ logger.always(JSON.stringify(metrics, null, 2));
178
184
  }
179
185
  else {
180
- console.log("");
181
- console.log(chalk.bold.cyan("=== Metrics Summary ==="));
182
- console.log("");
186
+ logger.always("");
187
+ logger.always(chalk.bold.cyan("=== Metrics Summary ==="));
188
+ logger.always("");
183
189
  // Request statistics
184
- console.log(chalk.bold("Request Statistics:"));
185
- console.log(formatRow(" Total requests:", metrics.totalSpans.toLocaleString()));
186
- console.log(formatRow(" Successful:", metrics.successfulSpans.toLocaleString()));
187
- console.log(formatRow(" Failed:", metrics.failedSpans.toLocaleString()));
188
- console.log(formatRow(" Success rate:", `${(metrics.successRate * 100).toFixed(2)}%`));
190
+ logger.always(chalk.bold("Request Statistics:"));
191
+ logger.always(formatRow(" Total requests:", metrics.totalSpans.toLocaleString()));
192
+ logger.always(formatRow(" Successful:", metrics.successfulSpans.toLocaleString()));
193
+ logger.always(formatRow(" Failed:", metrics.failedSpans.toLocaleString()));
194
+ logger.always(formatRow(" Success rate:", `${(metrics.successRate * 100).toFixed(2)}%`));
189
195
  // Latency statistics
190
196
  if (metrics.latency && metrics.latency.count > 0) {
191
- console.log("");
192
- console.log(chalk.bold("Latency (ms):"));
193
- console.log(formatRow(" Min:", metrics.latency.min.toFixed(2)));
194
- console.log(formatRow(" Max:", metrics.latency.max.toFixed(2)));
195
- console.log(formatRow(" Mean:", metrics.latency.mean.toFixed(2)));
196
- console.log(formatRow(" P50:", metrics.latency.p50.toFixed(2)));
197
- console.log(formatRow(" P95:", metrics.latency.p95.toFixed(2)));
198
- console.log(formatRow(" P99:", metrics.latency.p99.toFixed(2)));
197
+ logger.always("");
198
+ logger.always(chalk.bold("Latency (ms):"));
199
+ logger.always(formatRow(" Min:", metrics.latency.min.toFixed(2)));
200
+ logger.always(formatRow(" Max:", metrics.latency.max.toFixed(2)));
201
+ logger.always(formatRow(" Mean:", metrics.latency.mean.toFixed(2)));
202
+ logger.always(formatRow(" P50:", metrics.latency.p50.toFixed(2)));
203
+ logger.always(formatRow(" P95:", metrics.latency.p95.toFixed(2)));
204
+ logger.always(formatRow(" P99:", metrics.latency.p99.toFixed(2)));
199
205
  if (args.detailed) {
200
- console.log(formatRow(" P75:", metrics.latency.p75.toFixed(2)));
201
- console.log(formatRow(" P90:", metrics.latency.p90.toFixed(2)));
202
- console.log(formatRow(" Std Dev:", metrics.latency.stdDev.toFixed(2)));
206
+ logger.always(formatRow(" P75:", metrics.latency.p75.toFixed(2)));
207
+ logger.always(formatRow(" P90:", metrics.latency.p90.toFixed(2)));
208
+ logger.always(formatRow(" Std Dev:", metrics.latency.stdDev.toFixed(2)));
203
209
  }
204
210
  }
205
211
  // Token statistics
206
212
  if (metrics.tokens) {
207
- console.log("");
208
- console.log(chalk.bold("Token Usage:"));
209
- console.log(formatRow(" Input tokens:", metrics.tokens.totalInputTokens.toLocaleString()));
210
- console.log(formatRow(" Output tokens:", metrics.tokens.totalOutputTokens.toLocaleString()));
211
- console.log(formatRow(" Total tokens:", metrics.tokens.totalTokens.toLocaleString()));
213
+ logger.always("");
214
+ logger.always(chalk.bold("Token Usage:"));
215
+ logger.always(formatRow(" Input tokens:", metrics.tokens.totalInputTokens.toLocaleString()));
216
+ logger.always(formatRow(" Output tokens:", metrics.tokens.totalOutputTokens.toLocaleString()));
217
+ logger.always(formatRow(" Total tokens:", metrics.tokens.totalTokens.toLocaleString()));
212
218
  if (args.detailed &&
213
219
  metrics.tokens.cacheReadTokens &&
214
220
  metrics.tokens.cacheReadTokens > 0) {
215
- console.log(formatRow(" Cache read:", metrics.tokens.cacheReadTokens.toLocaleString()));
221
+ logger.always(formatRow(" Cache read:", metrics.tokens.cacheReadTokens.toLocaleString()));
216
222
  }
217
223
  if (args.detailed &&
218
224
  metrics.tokens.reasoningTokens &&
219
225
  metrics.tokens.reasoningTokens > 0) {
220
- console.log(formatRow(" Reasoning:", metrics.tokens.reasoningTokens.toLocaleString()));
226
+ logger.always(formatRow(" Reasoning:", metrics.tokens.reasoningTokens.toLocaleString()));
221
227
  }
222
228
  }
223
229
  // Cost
224
230
  if (metrics.totalCost !== undefined && metrics.totalCost > 0) {
225
- console.log("");
226
- console.log(chalk.bold("Cost:"));
227
- console.log(formatRow(" Total:", formatCost(metrics.totalCost)));
231
+ logger.always("");
232
+ logger.always(chalk.bold("Cost:"));
233
+ logger.always(formatRow(" Total:", formatCost(metrics.totalCost)));
228
234
  }
229
235
  // Tracking duration
230
236
  if (metrics.trackingDurationMs) {
231
237
  const durationSec = metrics.trackingDurationMs / 1000;
232
238
  const throughput = metrics.totalSpans > 0 ? metrics.totalSpans / durationSec : 0;
233
- console.log("");
234
- console.log(chalk.gray(`Tracking: ${durationSec.toFixed(1)}s (${throughput.toFixed(2)} req/s)`));
239
+ logger.always("");
240
+ logger.always(chalk.gray(`Tracking: ${durationSec.toFixed(1)}s (${throughput.toFixed(2)} req/s)`));
235
241
  }
236
- console.log("");
242
+ logger.always("");
237
243
  }
238
244
  }
239
245
  catch (error) {
240
- if (spinner)
246
+ if (spinner) {
241
247
  spinner.fail("Failed to get metrics");
248
+ }
242
249
  logger.error("Error:", error instanceof Error ? error.message : String(error));
243
250
  process.exit(1);
244
251
  }
@@ -276,60 +283,62 @@ export class ObservabilityCommandFactory {
276
283
  try {
277
284
  const neurolink = new NeuroLink();
278
285
  const status = neurolink.getTelemetryStatus();
279
- if (spinner)
286
+ if (spinner) {
280
287
  spinner.succeed("Exporters retrieved");
288
+ }
281
289
  const exporters = status.exporters ?? [];
282
290
  if (args.format === "json") {
283
- console.log(JSON.stringify(exporters, null, 2));
291
+ logger.always(JSON.stringify(exporters, null, 2));
284
292
  }
285
293
  else {
286
- console.log("");
287
- console.log(chalk.bold.cyan("=== Configured Exporters ==="));
288
- console.log("");
294
+ logger.always("");
295
+ logger.always(chalk.bold.cyan("=== Configured Exporters ==="));
296
+ logger.always("");
289
297
  if (exporters.length === 0) {
290
- console.log(chalk.gray("No exporters configured."));
291
- console.log("");
292
- console.log("Available exporters:");
293
- console.log(" - Langfuse (langfuse)");
294
- console.log(" - LangSmith (langsmith)");
295
- console.log(" - OpenTelemetry (otel)");
296
- console.log(" - Datadog (datadog)");
297
- console.log(" - Sentry (sentry)");
298
- console.log(" - Braintrust (braintrust)");
299
- console.log(" - Arize (arize)");
300
- console.log(" - PostHog (posthog)");
301
- console.log(" - Laminar (laminar)");
298
+ logger.always(chalk.gray("No exporters configured."));
299
+ logger.always("");
300
+ logger.always("Available exporters:");
301
+ logger.always(" - Langfuse (langfuse)");
302
+ logger.always(" - LangSmith (langsmith)");
303
+ logger.always(" - OpenTelemetry (otel)");
304
+ logger.always(" - Datadog (datadog)");
305
+ logger.always(" - Sentry (sentry)");
306
+ logger.always(" - Braintrust (braintrust)");
307
+ logger.always(" - Arize (arize)");
308
+ logger.always(" - PostHog (posthog)");
309
+ logger.always(" - Laminar (laminar)");
302
310
  }
303
311
  else {
304
312
  for (const exporter of exporters) {
305
313
  const healthIcon = exporter.healthy
306
314
  ? chalk.green("[OK]")
307
315
  : chalk.red("[ERROR]");
308
- console.log(`${healthIcon} ${chalk.bold(exporter.name)}`);
316
+ logger.always(`${healthIcon} ${chalk.bold(exporter.name)}`);
309
317
  if (exporter.latencyMs !== undefined) {
310
- console.log(formatRow(" Latency:", `${exporter.latencyMs.toFixed(2)}ms`));
318
+ logger.always(formatRow(" Latency:", `${exporter.latencyMs.toFixed(2)}ms`));
311
319
  }
312
320
  if (exporter.pendingSpans !== undefined) {
313
- console.log(formatRow(" Pending spans:", exporter.pendingSpans.toLocaleString()));
321
+ logger.always(formatRow(" Pending spans:", exporter.pendingSpans.toLocaleString()));
314
322
  }
315
323
  if (exporter.lastExportTime) {
316
324
  const lastExport = new Date(exporter.lastExportTime);
317
- console.log(formatRow(" Last export:", lastExport.toISOString()));
325
+ logger.always(formatRow(" Last export:", lastExport.toISOString()));
318
326
  }
319
327
  if (exporter.errors && exporter.errors.length > 0) {
320
- console.log(chalk.yellow(" Errors:"));
328
+ logger.always(chalk.yellow(" Errors:"));
321
329
  for (const error of exporter.errors.slice(0, 3)) {
322
- console.log(chalk.red(` - ${error}`));
330
+ logger.always(chalk.red(` - ${error}`));
323
331
  }
324
332
  }
325
- console.log("");
333
+ logger.always("");
326
334
  }
327
335
  }
328
336
  }
329
337
  }
330
338
  catch (error) {
331
- if (spinner)
339
+ if (spinner) {
332
340
  spinner.fail("Failed to get exporters");
341
+ }
333
342
  logger.error("Error:", error instanceof Error ? error.message : String(error));
334
343
  process.exit(1);
335
344
  }
@@ -377,8 +386,9 @@ export class ObservabilityCommandFactory {
377
386
  try {
378
387
  const neurolink = new NeuroLink();
379
388
  const metrics = neurolink.getMetrics();
380
- if (spinner)
389
+ if (spinner) {
381
390
  spinner.succeed("Costs calculated");
391
+ }
382
392
  if (args.format === "json") {
383
393
  const jsonOutput = {
384
394
  totalCost: metrics.totalCost,
@@ -389,60 +399,61 @@ export class ObservabilityCommandFactory {
389
399
  if (args.byModel !== false) {
390
400
  jsonOutput.costByModel = metrics.costByModel;
391
401
  }
392
- console.log(JSON.stringify(jsonOutput, null, 2));
402
+ logger.always(JSON.stringify(jsonOutput, null, 2));
393
403
  }
394
404
  else {
395
- console.log("");
396
- console.log(chalk.bold.cyan("=== Cost Breakdown ==="));
397
- console.log("");
405
+ logger.always("");
406
+ logger.always(chalk.bold.cyan("=== Cost Breakdown ==="));
407
+ logger.always("");
398
408
  // Total cost
399
- console.log(chalk.bold(`Total Cost: ${formatCost(metrics.totalCost ?? 0)}`));
409
+ logger.always(chalk.bold(`Total Cost: ${formatCost(metrics.totalCost ?? 0)}`));
400
410
  // Cost by provider
401
411
  if (args.byProvider !== false &&
402
412
  metrics.costByProvider &&
403
413
  metrics.costByProvider.length > 0) {
404
- console.log("");
405
- console.log(chalk.bold("By Provider:"));
414
+ logger.always("");
415
+ logger.always(chalk.bold("By Provider:"));
406
416
  // Sort by cost descending
407
417
  const sortedProviders = [...metrics.costByProvider].sort((a, b) => b.totalCost - a.totalCost);
408
418
  for (const provider of sortedProviders) {
409
419
  const costStr = formatCost(provider.totalCost);
410
420
  const avgCost = formatCost(provider.avgCostPerRequest);
411
- console.log(` ${chalk.cyan(provider.provider.padEnd(15))} ${costStr}`);
412
- console.log(chalk.gray(` ${provider.requestCount} requests, avg ${avgCost}/req`));
421
+ logger.always(` ${chalk.cyan(provider.provider.padEnd(15))} ${costStr}`);
422
+ logger.always(chalk.gray(` ${provider.requestCount} requests, avg ${avgCost}/req`));
413
423
  }
414
424
  }
415
425
  // Cost by model
416
426
  if (args.byModel !== false &&
417
427
  metrics.costByModel &&
418
428
  metrics.costByModel.length > 0) {
419
- console.log("");
420
- console.log(chalk.bold("By Model:"));
429
+ logger.always("");
430
+ logger.always(chalk.bold("By Model:"));
421
431
  // Sort by cost descending
422
432
  const sortedModels = [...metrics.costByModel].sort((a, b) => b.totalCost - a.totalCost);
423
433
  for (const model of sortedModels) {
424
434
  const costStr = formatCost(model.totalCost);
425
435
  const avgCost = formatCost(model.avgCostPerRequest);
426
- console.log(` ${chalk.cyan(model.model)}`);
427
- console.log(` Cost: ${costStr}`);
428
- console.log(chalk.gray(` ${model.requestCount} requests, avg ${avgCost}/req`));
429
- console.log(chalk.gray(` ${model.inputTokens.toLocaleString()} input, ${model.outputTokens.toLocaleString()} output tokens`));
436
+ logger.always(` ${chalk.cyan(model.model)}`);
437
+ logger.always(` Cost: ${costStr}`);
438
+ logger.always(chalk.gray(` ${model.requestCount} requests, avg ${avgCost}/req`));
439
+ logger.always(chalk.gray(` ${model.inputTokens.toLocaleString()} input, ${model.outputTokens.toLocaleString()} output tokens`));
430
440
  }
431
441
  }
432
442
  // No cost data
433
443
  if ((!metrics.costByProvider ||
434
444
  metrics.costByProvider.length === 0) &&
435
445
  (!metrics.costByModel || metrics.costByModel.length === 0)) {
436
- console.log("");
437
- console.log(chalk.gray("No cost data available."));
438
- console.log(chalk.gray("Cost tracking is recorded when observability is enabled."));
446
+ logger.always("");
447
+ logger.always(chalk.gray("No cost data available."));
448
+ logger.always(chalk.gray("Cost tracking is recorded when observability is enabled."));
439
449
  }
440
- console.log("");
450
+ logger.always("");
441
451
  }
442
452
  }
443
453
  catch (error) {
444
- if (spinner)
454
+ if (spinner) {
445
455
  spinner.fail("Failed to get costs");
456
+ }
446
457
  logger.error("Error:", error instanceof Error ? error.message : String(error));
447
458
  process.exit(1);
448
459
  }
@@ -0,0 +1,3 @@
1
+ import type { CommandModule } from "yargs";
2
+ import type { ProxyReplayArgs } from "../../lib/types/index.js";
3
+ export declare const proxyReplayCommand: CommandModule<object, ProxyReplayArgs>;
@@ -0,0 +1,175 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import chalk from "chalk";
4
+ import { compareProxyReplayBundle, exportProxyReplayBundle, isValidProxyReplayHeaderName, readProxyReplayBundle, writeProxyReplayDocument, } from "../../lib/proxy/proxyReplay.js";
5
+ import { logger } from "../../lib/utils/logger.js";
6
+ const ACTIONS = ["export", "compare"];
7
+ const MAX_BODY_OVERRIDE_BYTES = 16 * 1024 * 1024;
8
+ function requiredValue(value, option) {
9
+ if (!value) {
10
+ throw new Error(`${option} is required for this action`);
11
+ }
12
+ return value;
13
+ }
14
+ function resolveHeaderValues(mappings) {
15
+ const values = {};
16
+ for (const mapping of mappings ?? []) {
17
+ const separator = mapping.indexOf("=");
18
+ if (separator <= 0 || separator === mapping.length - 1) {
19
+ throw new Error(`Invalid --header-env value "${mapping}". Use header-name=ENV_VAR.`);
20
+ }
21
+ const header = mapping.slice(0, separator).trim().toLowerCase();
22
+ if (!isValidProxyReplayHeaderName(header)) {
23
+ throw new Error(`Invalid header name in --header-env: ${header}`);
24
+ }
25
+ const environmentName = mapping.slice(separator + 1).trim();
26
+ const value = process.env[environmentName];
27
+ if (!value) {
28
+ throw new Error(`Environment variable ${environmentName} is empty or unset for header ${header}`);
29
+ }
30
+ values[header] = value;
31
+ }
32
+ return values;
33
+ }
34
+ async function readBodyOverride(filePath) {
35
+ if (!filePath) {
36
+ return undefined;
37
+ }
38
+ const resolvedPath = resolve(filePath);
39
+ const info = await stat(resolvedPath);
40
+ if (!info.isFile() || info.size > MAX_BODY_OVERRIDE_BYTES) {
41
+ throw new Error(`Body override must be a regular file no larger than ${MAX_BODY_OVERRIDE_BYTES} bytes`);
42
+ }
43
+ return readFile(resolvedPath, "utf8");
44
+ }
45
+ function printResult(format, value) {
46
+ logger.always(format === "json"
47
+ ? JSON.stringify(value)
48
+ : Object.entries(value)
49
+ .map(([key, child]) => `${key}: ${String(child)}`)
50
+ .join("\n"));
51
+ }
52
+ export const proxyReplayCommand = {
53
+ command: "replay <action>",
54
+ describe: "Export captured proxy requests or compare them directly upstream",
55
+ builder: (yargs) => yargs
56
+ .positional("action", {
57
+ type: "string",
58
+ choices: [...ACTIONS],
59
+ describe: "Replay action",
60
+ })
61
+ .option("request-id", {
62
+ type: "string",
63
+ alias: "requestId",
64
+ description: "Captured proxy request ID to export",
65
+ })
66
+ .option("attempt", {
67
+ type: "number",
68
+ description: "Specific upstream attempt to reconstruct",
69
+ })
70
+ .option("logs-dir", {
71
+ type: "string",
72
+ alias: "logsDir",
73
+ description: "Proxy log directory",
74
+ })
75
+ .option("bundle", {
76
+ type: "string",
77
+ description: "Replay bundle to compare directly upstream",
78
+ })
79
+ .option("output", {
80
+ type: "string",
81
+ alias: "o",
82
+ description: "Destination JSON file",
83
+ })
84
+ .option("execute", {
85
+ type: "boolean",
86
+ default: false,
87
+ description: "Explicitly permit the direct upstream request",
88
+ })
89
+ .option("header-env", {
90
+ type: "string",
91
+ array: true,
92
+ alias: "headerEnv",
93
+ description: "Inject a redacted header from an environment variable (header=ENV_VAR)",
94
+ })
95
+ .option("body-file", {
96
+ type: "string",
97
+ alias: "bodyFile",
98
+ description: "Complete request body override for redacted/truncated captures",
99
+ })
100
+ .option("url", {
101
+ type: "string",
102
+ description: "Explicit direct endpoint override",
103
+ })
104
+ .option("timeout-ms", {
105
+ type: "number",
106
+ alias: "timeoutMs",
107
+ default: 120_000,
108
+ description: "Direct request timeout",
109
+ })
110
+ .option("format", {
111
+ type: "string",
112
+ choices: ["text", "json"],
113
+ default: "text",
114
+ description: "Command summary format",
115
+ })
116
+ .option("quiet", {
117
+ type: "boolean",
118
+ alias: "q",
119
+ default: false,
120
+ description: "Suppress non-essential output",
121
+ })
122
+ .example("neurolink proxy replay export --request-id req-123 --output replay.json", "Create a deterministic offline replay bundle")
123
+ .example("neurolink proxy replay compare --bundle replay.json --output comparison.json --execute --header-env authorization=ANTHROPIC_AUTHORIZATION", "Execute the captured upstream request with an environment-backed credential"),
124
+ handler: async (argv) => {
125
+ try {
126
+ const output = requiredValue(argv.output, "--output");
127
+ if (argv.action === "export") {
128
+ const requestId = requiredValue(argv.requestId, "--request-id");
129
+ const bundle = await exportProxyReplayBundle({
130
+ requestId,
131
+ logsDir: argv.logsDir,
132
+ attempt: argv.attempt,
133
+ });
134
+ const written = await writeProxyReplayDocument(output, bundle);
135
+ if (!argv.quiet) {
136
+ printResult(argv.format ?? "text", {
137
+ action: "export",
138
+ output: written.path,
139
+ sha256: written.sha256,
140
+ captures: bundle.completeness.captures,
141
+ replayable: bundle.completeness.replayable,
142
+ blockers: bundle.completeness.blockers.join(",") || "none",
143
+ });
144
+ }
145
+ return;
146
+ }
147
+ const bundlePath = requiredValue(argv.bundle, "--bundle");
148
+ const bundle = await readProxyReplayBundle(bundlePath);
149
+ const comparison = await compareProxyReplayBundle(bundle, {
150
+ execute: argv.execute === true,
151
+ headerValues: resolveHeaderValues(argv.headerEnv),
152
+ bodyOverride: await readBodyOverride(argv.bodyFile),
153
+ urlOverride: argv.url,
154
+ timeoutMs: argv.timeoutMs,
155
+ });
156
+ const written = await writeProxyReplayDocument(output, comparison);
157
+ if (!argv.quiet) {
158
+ printResult(argv.format ?? "text", {
159
+ action: "compare",
160
+ output: written.path,
161
+ sha256: written.sha256,
162
+ status: comparison.direct.status,
163
+ statusMatches: comparison.comparison.statusMatches,
164
+ bodyHashMatches: comparison.comparison.bodyHashMatches,
165
+ jsonShapeMatches: comparison.comparison.jsonShapeMatches,
166
+ });
167
+ }
168
+ }
169
+ catch (error) {
170
+ logger.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
171
+ process.exitCode = 1;
172
+ }
173
+ },
174
+ };
175
+ //# sourceMappingURL=proxyReplay.js.map