@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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +305 -305
- package/dist/cli/commands/observability.js +114 -103
- package/dist/cli/commands/proxyReplay.d.ts +3 -0
- package/dist/cli/commands/proxyReplay.js +175 -0
- package/dist/cli/commands/telemetry.js +139 -119
- package/dist/cli/parser.js +3 -1
- package/dist/lib/proxy/proxyReplay.d.ts +10 -0
- package/dist/lib/proxy/proxyReplay.js +765 -0
- package/dist/lib/proxy/requestLogger.d.ts +13 -0
- package/dist/lib/proxy/requestLogger.js +13 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +9 -2
- package/dist/lib/types/cli.d.ts +16 -0
- package/dist/lib/types/proxy.d.ts +121 -0
- package/dist/proxy/proxyReplay.d.ts +10 -0
- package/dist/proxy/proxyReplay.js +764 -0
- package/dist/proxy/requestLogger.d.ts +13 -0
- package/dist/proxy/requestLogger.js +13 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +9 -2
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/proxy.d.ts +121 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
90
|
+
logger.always(formatRow("Telemetry:", enabledIcon));
|
|
87
91
|
// Langfuse status
|
|
88
92
|
if (status.langfuse) {
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
98
|
+
logger.always(formatRow(" Status:", lfStatus));
|
|
95
99
|
if (status.langfuse.baseUrl) {
|
|
96
|
-
|
|
100
|
+
logger.always(formatRow(" URL:", redactUrlCredentials(status.langfuse.baseUrl)));
|
|
97
101
|
}
|
|
98
102
|
if (status.langfuse.environment) {
|
|
99
|
-
|
|
103
|
+
logger.always(formatRow(" Environment:", status.langfuse.environment));
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
// OpenTelemetry status
|
|
103
107
|
if (status.openTelemetry) {
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
113
|
+
logger.always(formatRow(" Status:", otelStatus));
|
|
110
114
|
if (status.openTelemetry.endpoint) {
|
|
111
|
-
|
|
115
|
+
logger.always(formatRow(" Endpoint:", redactUrlCredentials(status.openTelemetry.endpoint)));
|
|
112
116
|
}
|
|
113
117
|
if (status.openTelemetry.serviceName) {
|
|
114
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
129
|
+
logger.always(formatRow(` ${exporter.name}:`, exporterStatus));
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
|
-
|
|
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
|
-
|
|
183
|
+
logger.always(JSON.stringify(metrics, null, 2));
|
|
178
184
|
}
|
|
179
185
|
else {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
logger.always("");
|
|
187
|
+
logger.always(chalk.bold.cyan("=== Metrics Summary ==="));
|
|
188
|
+
logger.always("");
|
|
183
189
|
// Request statistics
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
234
|
-
|
|
239
|
+
logger.always("");
|
|
240
|
+
logger.always(chalk.gray(`Tracking: ${durationSec.toFixed(1)}s (${throughput.toFixed(2)} req/s)`));
|
|
235
241
|
}
|
|
236
|
-
|
|
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
|
-
|
|
291
|
+
logger.always(JSON.stringify(exporters, null, 2));
|
|
284
292
|
}
|
|
285
293
|
else {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
294
|
+
logger.always("");
|
|
295
|
+
logger.always(chalk.bold.cyan("=== Configured Exporters ==="));
|
|
296
|
+
logger.always("");
|
|
289
297
|
if (exporters.length === 0) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
316
|
+
logger.always(`${healthIcon} ${chalk.bold(exporter.name)}`);
|
|
309
317
|
if (exporter.latencyMs !== undefined) {
|
|
310
|
-
|
|
318
|
+
logger.always(formatRow(" Latency:", `${exporter.latencyMs.toFixed(2)}ms`));
|
|
311
319
|
}
|
|
312
320
|
if (exporter.pendingSpans !== undefined) {
|
|
313
|
-
|
|
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
|
-
|
|
325
|
+
logger.always(formatRow(" Last export:", lastExport.toISOString()));
|
|
318
326
|
}
|
|
319
327
|
if (exporter.errors && exporter.errors.length > 0) {
|
|
320
|
-
|
|
328
|
+
logger.always(chalk.yellow(" Errors:"));
|
|
321
329
|
for (const error of exporter.errors.slice(0, 3)) {
|
|
322
|
-
|
|
330
|
+
logger.always(chalk.red(` - ${error}`));
|
|
323
331
|
}
|
|
324
332
|
}
|
|
325
|
-
|
|
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
|
-
|
|
402
|
+
logger.always(JSON.stringify(jsonOutput, null, 2));
|
|
393
403
|
}
|
|
394
404
|
else {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
405
|
+
logger.always("");
|
|
406
|
+
logger.always(chalk.bold.cyan("=== Cost Breakdown ==="));
|
|
407
|
+
logger.always("");
|
|
398
408
|
// Total cost
|
|
399
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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
|
-
|
|
412
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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,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
|