@juspay/neurolink 9.41.0 → 9.42.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.
Files changed (189) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +7 -1
  3. package/dist/auth/anthropicOAuth.d.ts +18 -3
  4. package/dist/auth/anthropicOAuth.js +137 -4
  5. package/dist/auth/providers/firebase.js +5 -1
  6. package/dist/auth/providers/jwt.js +5 -1
  7. package/dist/auth/providers/workos.js +5 -1
  8. package/dist/auth/sessionManager.d.ts +1 -1
  9. package/dist/auth/sessionManager.js +58 -27
  10. package/dist/browser/neurolink.min.js +337 -318
  11. package/dist/cli/commands/mcp.js +3 -0
  12. package/dist/cli/commands/proxy.d.ts +2 -1
  13. package/dist/cli/commands/proxy.js +279 -16
  14. package/dist/cli/commands/task.js +3 -0
  15. package/dist/cli/factories/commandFactory.d.ts +2 -0
  16. package/dist/cli/factories/commandFactory.js +38 -0
  17. package/dist/cli/parser.js +4 -3
  18. package/dist/client/aiSdkAdapter.js +3 -0
  19. package/dist/client/streamingClient.js +30 -10
  20. package/dist/core/modules/GenerationHandler.js +3 -2
  21. package/dist/core/redisConversationMemoryManager.js +7 -3
  22. package/dist/evaluation/BatchEvaluator.js +4 -1
  23. package/dist/evaluation/hooks/observabilityHooks.js +5 -3
  24. package/dist/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  25. package/dist/evaluation/pipeline/evaluationPipeline.js +20 -8
  26. package/dist/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  27. package/dist/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  28. package/dist/lib/auth/anthropicOAuth.d.ts +18 -3
  29. package/dist/lib/auth/anthropicOAuth.js +137 -4
  30. package/dist/lib/auth/providers/firebase.js +5 -1
  31. package/dist/lib/auth/providers/jwt.js +5 -1
  32. package/dist/lib/auth/providers/workos.js +5 -1
  33. package/dist/lib/auth/sessionManager.d.ts +1 -1
  34. package/dist/lib/auth/sessionManager.js +58 -27
  35. package/dist/lib/client/aiSdkAdapter.js +3 -0
  36. package/dist/lib/client/streamingClient.js +30 -10
  37. package/dist/lib/core/modules/GenerationHandler.js +3 -2
  38. package/dist/lib/core/redisConversationMemoryManager.js +7 -3
  39. package/dist/lib/evaluation/BatchEvaluator.js +4 -1
  40. package/dist/lib/evaluation/hooks/observabilityHooks.js +5 -3
  41. package/dist/lib/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  42. package/dist/lib/evaluation/pipeline/evaluationPipeline.js +20 -8
  43. package/dist/lib/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  44. package/dist/lib/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  45. package/dist/lib/neurolink.d.ts +3 -2
  46. package/dist/lib/neurolink.js +260 -494
  47. package/dist/lib/observability/otelBridge.d.ts +2 -2
  48. package/dist/lib/observability/otelBridge.js +12 -3
  49. package/dist/lib/providers/amazonBedrock.js +2 -4
  50. package/dist/lib/providers/anthropic.d.ts +9 -5
  51. package/dist/lib/providers/anthropic.js +19 -14
  52. package/dist/lib/providers/anthropicBaseProvider.d.ts +3 -3
  53. package/dist/lib/providers/anthropicBaseProvider.js +5 -4
  54. package/dist/lib/providers/azureOpenai.d.ts +1 -1
  55. package/dist/lib/providers/azureOpenai.js +5 -4
  56. package/dist/lib/providers/googleAiStudio.js +30 -1
  57. package/dist/lib/providers/googleVertex.js +28 -6
  58. package/dist/lib/providers/huggingFace.d.ts +3 -3
  59. package/dist/lib/providers/huggingFace.js +6 -8
  60. package/dist/lib/providers/litellm.js +41 -29
  61. package/dist/lib/providers/mistral.js +2 -1
  62. package/dist/lib/providers/ollama.js +80 -23
  63. package/dist/lib/providers/openAI.js +3 -2
  64. package/dist/lib/providers/openRouter.js +2 -1
  65. package/dist/lib/providers/openaiCompatible.d.ts +4 -4
  66. package/dist/lib/providers/openaiCompatible.js +4 -4
  67. package/dist/lib/proxy/claudeFormat.d.ts +3 -2
  68. package/dist/lib/proxy/claudeFormat.js +25 -20
  69. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  70. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  71. package/dist/lib/proxy/modelRouter.js +3 -0
  72. package/dist/lib/proxy/oauthFetch.d.ts +1 -1
  73. package/dist/lib/proxy/oauthFetch.js +65 -72
  74. package/dist/lib/proxy/proxyConfig.js +44 -24
  75. package/dist/lib/proxy/proxyEnv.d.ts +19 -0
  76. package/dist/lib/proxy/proxyEnv.js +73 -0
  77. package/dist/lib/proxy/proxyFetch.js +50 -4
  78. package/dist/lib/proxy/proxyTracer.d.ts +133 -0
  79. package/dist/lib/proxy/proxyTracer.js +645 -0
  80. package/dist/lib/proxy/rawStreamCapture.d.ts +10 -0
  81. package/dist/lib/proxy/rawStreamCapture.js +83 -0
  82. package/dist/lib/proxy/requestLogger.d.ts +32 -5
  83. package/dist/lib/proxy/requestLogger.js +406 -37
  84. package/dist/lib/proxy/sseInterceptor.d.ts +97 -0
  85. package/dist/lib/proxy/sseInterceptor.js +402 -0
  86. package/dist/lib/proxy/usageStats.d.ts +4 -3
  87. package/dist/lib/proxy/usageStats.js +25 -12
  88. package/dist/lib/rag/chunkers/MarkdownChunker.js +13 -5
  89. package/dist/lib/rag/chunking/markdownChunker.js +15 -6
  90. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +7 -2
  91. package/dist/lib/server/routes/claudeProxyRoutes.js +1737 -508
  92. package/dist/lib/services/server/ai/observability/instrumentation.d.ts +7 -1
  93. package/dist/lib/services/server/ai/observability/instrumentation.js +240 -40
  94. package/dist/lib/tasks/backends/bullmqBackend.d.ts +1 -0
  95. package/dist/lib/tasks/backends/bullmqBackend.js +14 -7
  96. package/dist/lib/tasks/store/redisTaskStore.d.ts +1 -0
  97. package/dist/lib/tasks/store/redisTaskStore.js +34 -26
  98. package/dist/lib/tasks/taskManager.d.ts +3 -0
  99. package/dist/lib/tasks/taskManager.js +63 -30
  100. package/dist/lib/telemetry/index.d.ts +2 -1
  101. package/dist/lib/telemetry/index.js +2 -1
  102. package/dist/lib/telemetry/telemetryService.d.ts +3 -0
  103. package/dist/lib/telemetry/telemetryService.js +65 -5
  104. package/dist/lib/types/cli.d.ts +10 -0
  105. package/dist/lib/types/proxyTypes.d.ts +37 -5
  106. package/dist/lib/types/streamTypes.d.ts +25 -3
  107. package/dist/lib/utils/messageBuilder.js +3 -2
  108. package/dist/lib/utils/providerHealth.d.ts +18 -0
  109. package/dist/lib/utils/providerHealth.js +240 -9
  110. package/dist/lib/utils/providerUtils.js +14 -8
  111. package/dist/lib/utils/toolChoice.d.ts +4 -0
  112. package/dist/lib/utils/toolChoice.js +7 -0
  113. package/dist/neurolink.d.ts +3 -2
  114. package/dist/neurolink.js +260 -494
  115. package/dist/observability/otelBridge.d.ts +2 -2
  116. package/dist/observability/otelBridge.js +12 -3
  117. package/dist/providers/amazonBedrock.js +2 -4
  118. package/dist/providers/anthropic.d.ts +9 -5
  119. package/dist/providers/anthropic.js +19 -14
  120. package/dist/providers/anthropicBaseProvider.d.ts +3 -3
  121. package/dist/providers/anthropicBaseProvider.js +5 -4
  122. package/dist/providers/azureOpenai.d.ts +1 -1
  123. package/dist/providers/azureOpenai.js +5 -4
  124. package/dist/providers/googleAiStudio.js +30 -1
  125. package/dist/providers/googleVertex.js +28 -6
  126. package/dist/providers/huggingFace.d.ts +3 -3
  127. package/dist/providers/huggingFace.js +6 -7
  128. package/dist/providers/litellm.js +41 -29
  129. package/dist/providers/mistral.js +2 -1
  130. package/dist/providers/ollama.js +80 -23
  131. package/dist/providers/openAI.js +3 -2
  132. package/dist/providers/openRouter.js +2 -1
  133. package/dist/providers/openaiCompatible.d.ts +4 -4
  134. package/dist/providers/openaiCompatible.js +4 -3
  135. package/dist/proxy/claudeFormat.d.ts +3 -2
  136. package/dist/proxy/claudeFormat.js +25 -20
  137. package/dist/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  138. package/dist/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  139. package/dist/proxy/modelRouter.js +3 -0
  140. package/dist/proxy/oauthFetch.d.ts +1 -1
  141. package/dist/proxy/oauthFetch.js +65 -72
  142. package/dist/proxy/proxyConfig.js +44 -24
  143. package/dist/proxy/proxyEnv.d.ts +19 -0
  144. package/dist/proxy/proxyEnv.js +72 -0
  145. package/dist/proxy/proxyFetch.js +50 -4
  146. package/dist/proxy/proxyTracer.d.ts +133 -0
  147. package/dist/proxy/proxyTracer.js +644 -0
  148. package/dist/proxy/rawStreamCapture.d.ts +10 -0
  149. package/dist/proxy/rawStreamCapture.js +82 -0
  150. package/dist/proxy/requestLogger.d.ts +32 -5
  151. package/dist/proxy/requestLogger.js +406 -37
  152. package/dist/proxy/sseInterceptor.d.ts +97 -0
  153. package/dist/proxy/sseInterceptor.js +401 -0
  154. package/dist/proxy/usageStats.d.ts +4 -3
  155. package/dist/proxy/usageStats.js +25 -12
  156. package/dist/rag/chunkers/MarkdownChunker.js +13 -5
  157. package/dist/rag/chunking/markdownChunker.js +15 -6
  158. package/dist/server/routes/claudeProxyRoutes.d.ts +7 -2
  159. package/dist/server/routes/claudeProxyRoutes.js +1737 -508
  160. package/dist/services/server/ai/observability/instrumentation.d.ts +7 -1
  161. package/dist/services/server/ai/observability/instrumentation.js +240 -40
  162. package/dist/tasks/backends/bullmqBackend.d.ts +1 -0
  163. package/dist/tasks/backends/bullmqBackend.js +14 -7
  164. package/dist/tasks/store/redisTaskStore.d.ts +1 -0
  165. package/dist/tasks/store/redisTaskStore.js +34 -26
  166. package/dist/tasks/taskManager.d.ts +3 -0
  167. package/dist/tasks/taskManager.js +63 -30
  168. package/dist/telemetry/index.d.ts +2 -1
  169. package/dist/telemetry/index.js +2 -1
  170. package/dist/telemetry/telemetryService.d.ts +3 -0
  171. package/dist/telemetry/telemetryService.js +65 -5
  172. package/dist/types/cli.d.ts +10 -0
  173. package/dist/types/proxyTypes.d.ts +37 -5
  174. package/dist/types/streamTypes.d.ts +25 -3
  175. package/dist/utils/messageBuilder.js +3 -2
  176. package/dist/utils/providerHealth.d.ts +18 -0
  177. package/dist/utils/providerHealth.js +240 -9
  178. package/dist/utils/providerUtils.js +14 -8
  179. package/dist/utils/toolChoice.d.ts +4 -0
  180. package/dist/utils/toolChoice.js +6 -0
  181. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +6609 -0
  182. package/docs/changelog.md +252 -0
  183. package/package.json +17 -1
  184. package/scripts/observability/check-proxy-telemetry.mjs +235 -0
  185. package/scripts/observability/docker-compose.proxy-observability.yaml +55 -0
  186. package/scripts/observability/import-openobserve-dashboard.mjs +240 -0
  187. package/scripts/observability/manage-local-openobserve.sh +184 -0
  188. package/scripts/observability/otel-collector.proxy-observability.yaml +78 -0
  189. package/scripts/observability/proxy-observability.env.example +23 -0
@@ -565,6 +565,7 @@ export class MCPCommandFactory {
565
565
  ? null
566
566
  : ora("Testing MCP server connections...").start();
567
567
  const sdk = new NeuroLink();
568
+ await sdk.getMCPStatus();
568
569
  let serversToTest = await sdk.listMCPServers();
569
570
  if (targetServer) {
570
571
  serversToTest = serversToTest.filter((s) => s.name === targetServer);
@@ -654,6 +655,7 @@ export class MCPCommandFactory {
654
655
  }
655
656
  }
656
657
  const sdk = new NeuroLink();
658
+ await sdk.getMCPStatus();
657
659
  // Check if server exists and is connected
658
660
  const allServers = await sdk.listMCPServers();
659
661
  const server = allServers.find((s) => s.name === serverName);
@@ -762,6 +764,7 @@ export class MCPCommandFactory {
762
764
  process.exit(1);
763
765
  }
764
766
  const sdk = new NeuroLink();
767
+ await sdk.getMCPStatus();
765
768
  const allServers = await sdk.listMCPServers();
766
769
  const server = allServers.find((s) => s.name === serverName);
767
770
  if (!server) {
@@ -10,10 +10,11 @@
10
10
  * (generate/stream), with an optional ModelRouter for model remapping.
11
11
  */
12
12
  import type { CommandModule } from "yargs";
13
- import type { ProxyStartArgs, ProxyStatusArgs, ProxyGuardArgs } from "../../lib/types/index.js";
13
+ import type { ProxyStartArgs, ProxyStatusArgs, ProxyGuardArgs, ProxyTelemetryArgs } from "../../lib/types/index.js";
14
14
  export declare function mapClaudeErrorTypeToStatus(errorType?: string): number;
15
15
  export declare const proxyStartCommand: CommandModule<object, ProxyStartArgs>;
16
16
  export declare const proxyStatusCommand: CommandModule<object, ProxyStatusArgs>;
17
+ export declare const proxyTelemetryCommand: CommandModule<object, ProxyTelemetryArgs>;
17
18
  export declare const proxyGuardCommand: CommandModule<object, ProxyGuardArgs>;
18
19
  export declare const proxySetupCommand: CommandModule;
19
20
  export declare const proxyInstallCommand: CommandModule;
@@ -16,9 +16,12 @@ import chalk from "chalk";
16
16
  import ora from "ora";
17
17
  import { logger } from "../../lib/utils/logger.js";
18
18
  import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
19
+ import { loadProxyEnvFile, resolveProxyEnvFile, } from "../../lib/proxy/proxyEnv.js";
19
20
  import { createRequire } from "node:module";
21
+ import { fileURLToPath } from "node:url";
20
22
  const _require = createRequire(import.meta.url);
21
23
  const { version: PROXY_VERSION } = _require("../../../package.json");
24
+ const PROXY_TELEMETRY_SCRIPT_PATH = fileURLToPath(new URL("../../../scripts/observability/manage-local-openobserve.sh", import.meta.url));
22
25
  // =============================================================================
23
26
  // STATE MANAGEMENT
24
27
  // =============================================================================
@@ -220,6 +223,32 @@ function spawnFailOpenGuard(host, port, parentPid) {
220
223
  return undefined;
221
224
  }
222
225
  }
226
+ async function runProxyTelemetryManager(command) {
227
+ const { existsSync } = await import("fs");
228
+ if (!existsSync(PROXY_TELEMETRY_SCRIPT_PATH)) {
229
+ throw new Error("Proxy telemetry helper files were not found in this installation. Reinstall NeuroLink with observability assets included.");
230
+ }
231
+ await new Promise((resolve, reject) => {
232
+ const child = spawn("bash", [PROXY_TELEMETRY_SCRIPT_PATH, command], {
233
+ stdio: "inherit",
234
+ env: process.env,
235
+ });
236
+ child.on("error", (error) => {
237
+ reject(error);
238
+ });
239
+ child.on("exit", (code, signal) => {
240
+ if (signal) {
241
+ reject(new Error(`proxy telemetry ${command} terminated by signal ${signal}`));
242
+ return;
243
+ }
244
+ if (code !== 0) {
245
+ reject(new Error(`proxy telemetry ${command} exited with code ${code ?? 1}`));
246
+ return;
247
+ }
248
+ resolve();
249
+ });
250
+ });
251
+ }
223
252
  // =============================================================================
224
253
  // STARTUP BANNER
225
254
  // =============================================================================
@@ -313,6 +342,16 @@ export const proxyStartCommand = {
313
342
  alias: "c",
314
343
  description: "Path to proxy config file (YAML/JSON)",
315
344
  defaultDescription: "~/.neurolink/proxy-config.yaml",
345
+ })
346
+ .option("env-file", {
347
+ type: "string",
348
+ alias: "envFile",
349
+ description: "Path to proxy provider env file (overrides cwd .env for the proxy process)",
350
+ })
351
+ .option("passthrough", {
352
+ type: "boolean",
353
+ default: false,
354
+ description: "Run in transparent passthrough mode (no retry, no rotation, no polyfill)",
316
355
  })
317
356
  .example("neurolink proxy start", "Start proxy on default port 55669 with fill-first strategy")
318
357
  .example("neurolink proxy start -p 8080 -s fill-first", "Start proxy on port 8080 with fill-first")
@@ -357,6 +396,22 @@ export const proxyStartCommand = {
357
396
  // -----------------------------------------------------------------
358
397
  // 1. Create NeuroLink instance — reads all env vars automatically
359
398
  // -----------------------------------------------------------------
399
+ let loadedEnvFile;
400
+ try {
401
+ const envResult = await loadProxyEnvFile({
402
+ explicitEnvFile: argv.envFile,
403
+ });
404
+ loadedEnvFile = envResult.path;
405
+ if (spinner && loadedEnvFile) {
406
+ spinner.text = `Loaded proxy env from ${loadedEnvFile}`;
407
+ }
408
+ }
409
+ catch (envError) {
410
+ if (spinner) {
411
+ spinner.fail(chalk.red(envError instanceof Error ? envError.message : String(envError)));
412
+ }
413
+ process.exit(1);
414
+ }
360
415
  // Skip MCP initialization for proxy — tools come from Claude Code, not MCP servers
361
416
  process.env.NEUROLINK_SKIP_MCP = "true";
362
417
  const { NeuroLink } = await import("../../lib/neurolink.js");
@@ -433,15 +488,24 @@ export const proxyStartCommand = {
433
488
  },
434
489
  }, 502);
435
490
  });
436
- const routeGroup = createClaudeProxyRoutes(modelRouter, "", strategy);
491
+ const passthrough = argv.passthrough ?? false;
492
+ const routeGroup = createClaudeProxyRoutes(modelRouter, "", strategy, passthrough);
437
493
  // Register proxy routes — inject NeuroLink into ServerContext
438
494
  for (const route of routeGroup.routes) {
439
495
  const method = route.method.toLowerCase();
440
496
  app[method](route.path, async (c) => {
441
497
  const emptyBody = {};
442
- const body = method === "post"
443
- ? await c.req.json().catch(() => emptyBody)
444
- : undefined;
498
+ let body;
499
+ let rawBody;
500
+ if (method === "post") {
501
+ rawBody = await c.req.text().catch(() => undefined);
502
+ try {
503
+ body = rawBody ? JSON.parse(rawBody) : emptyBody;
504
+ }
505
+ catch {
506
+ body = emptyBody;
507
+ }
508
+ }
445
509
  // Log incoming request
446
510
  const model = body?.model ?? "-";
447
511
  const stream = body?.stream
@@ -461,6 +525,7 @@ export const proxyStartCommand = {
461
525
  query: Object.fromEntries(new URL(c.req.url).searchParams.entries()),
462
526
  params: c.req.param(),
463
527
  body,
528
+ rawBody, // Preserve original bytes for passthrough mode
464
529
  neurolink, // NeuroLink instance for generate/stream
465
530
  toolRegistry: neurolink.getToolRegistry(),
466
531
  timestamp: Date.now(),
@@ -564,6 +629,7 @@ export const proxyStartCommand = {
564
629
  uptime: process.uptime(),
565
630
  version: PROXY_VERSION,
566
631
  stats: {
632
+ totalAttempts: stats.totalAttempts,
567
633
  totalRequests: stats.totalRequests,
568
634
  totalSuccess: stats.totalSuccess,
569
635
  totalErrors: stats.totalErrors,
@@ -571,7 +637,8 @@ export const proxyStartCommand = {
571
637
  accounts: Object.values(stats.accounts).map((a) => ({
572
638
  label: a.label,
573
639
  type: a.type,
574
- requests: a.requestCount,
640
+ attempts: a.attemptCount,
641
+ requests: a.attemptCount,
575
642
  success: a.successCount,
576
643
  errors: a.errorCount,
577
644
  rateLimits: a.rateLimitCount,
@@ -583,7 +650,55 @@ export const proxyStartCommand = {
583
650
  });
584
651
  });
585
652
  // -----------------------------------------------------------------
586
- // 5. Start listening
653
+ // 5. Initialize OpenTelemetry for proxy observability
654
+ // -----------------------------------------------------------------
655
+ try {
656
+ const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
657
+ if (!process.env.OTEL_SERVICE_NAME) {
658
+ process.env.OTEL_SERVICE_NAME = "neurolink-proxy";
659
+ }
660
+ // Merge resource attributes (preserving any existing ones)
661
+ process.env.OTEL_RESOURCE_ATTRIBUTES = [
662
+ `service.name=neurolink-proxy`,
663
+ `service.version=${PROXY_VERSION}`,
664
+ `deployment.environment=local`,
665
+ process.env.OTEL_RESOURCE_ATTRIBUTES,
666
+ ]
667
+ .filter(Boolean)
668
+ .join(",");
669
+ const { initializeOpenTelemetry, isOpenTelemetryInitialized } = await import("../../lib/services/server/ai/observability/instrumentation.js");
670
+ const { buildObservabilityConfigFromEnv } = await import("../../lib/utils/observabilityHelpers.js");
671
+ if (!isOpenTelemetryInitialized()) {
672
+ const observabilityConfig = buildObservabilityConfigFromEnv();
673
+ const langfuseConfig = observabilityConfig?.langfuse;
674
+ const langfuseEnabled = langfuseConfig?.enabled === true;
675
+ initializeOpenTelemetry({
676
+ enabled: langfuseEnabled,
677
+ publicKey: langfuseConfig?.publicKey || "",
678
+ secretKey: langfuseConfig?.secretKey || "",
679
+ baseUrl: langfuseConfig?.baseUrl,
680
+ environment: "proxy",
681
+ release: PROXY_VERSION,
682
+ userId: "neurolink-proxy",
683
+ autoDetectOperationName: true,
684
+ });
685
+ if (langfuseEnabled) {
686
+ logger.always(`[proxy] Langfuse enabled — exporting to ${langfuseConfig.baseUrl || "https://cloud.langfuse.com"} (environment=proxy)`);
687
+ }
688
+ if (otlpEndpoint) {
689
+ logger.always(`[proxy] OTLP exporter enabled — exporting to ${otlpEndpoint} (service.name=neurolink-proxy)`);
690
+ }
691
+ if (!langfuseEnabled && !otlpEndpoint) {
692
+ logger.always("[proxy] OpenTelemetry exporters disabled — set OTEL_EXPORTER_OTLP_ENDPOINT or Langfuse credentials to enable proxy observability");
693
+ }
694
+ }
695
+ }
696
+ catch (otelError) {
697
+ // OTel is non-critical — proxy must still work without it
698
+ logger.debug(`[proxy] OpenTelemetry init failed (non-fatal): ${otelError instanceof Error ? otelError.message : String(otelError)}`);
699
+ }
700
+ // -----------------------------------------------------------------
701
+ // 6. Start listening
587
702
  // -----------------------------------------------------------------
588
703
  const port = argv.port ?? 55669;
589
704
  const host = argv.host ?? "127.0.0.1";
@@ -608,9 +723,13 @@ export const proxyStartCommand = {
608
723
  host,
609
724
  strategy,
610
725
  startTime: new Date().toISOString(),
726
+ envFile: loadedEnvFile,
611
727
  fallbackChain,
612
728
  guardPid,
613
- managedBy: "manual",
729
+ managedBy: process.platform === "darwin" && process.ppid === 1
730
+ ? "launchd"
731
+ : "manual",
732
+ passthrough,
614
733
  };
615
734
  saveProxyState(state);
616
735
  if (spinner) {
@@ -619,6 +738,10 @@ export const proxyStartCommand = {
619
738
  const normalizedHost = host === "0.0.0.0" ? "localhost" : host;
620
739
  const url = `http://${normalizedHost}:${port}`;
621
740
  printProxyBanner(url, strategy);
741
+ logger.always(` ${chalk.bold("Mode:")} ${chalk.cyan(passthrough ? "passthrough" : "full")}`);
742
+ if (loadedEnvFile) {
743
+ logger.always(` ${chalk.bold("Env File:")} ${chalk.cyan(loadedEnvFile)}`);
744
+ }
622
745
  // Auto-configure Claude Code — use the normalized URL (localhost, not 0.0.0.0)
623
746
  try {
624
747
  await setClaudeProxySettings(url);
@@ -630,7 +753,7 @@ export const proxyStartCommand = {
630
753
  (e instanceof Error ? e.message : String(e)));
631
754
  }
632
755
  // -----------------------------------------------------------------
633
- // 6. Background token refresh (every 30 seconds)
756
+ // 7. Background token refresh (every 30 seconds)
634
757
  // -----------------------------------------------------------------
635
758
  const { needsRefresh, refreshToken, persistTokens } = await import("../../lib/proxy/tokenRefresh.js");
636
759
  const { tokenStore } = await import("../../lib/auth/tokenStore.js");
@@ -697,12 +820,21 @@ export const proxyStartCommand = {
697
820
  cleanupLogs(7, 500);
698
821
  }, 60 * 60 * 1000);
699
822
  // -----------------------------------------------------------------
700
- // 7. Graceful shutdown
823
+ // 8. Graceful shutdown
701
824
  // -----------------------------------------------------------------
702
825
  const shutdown = async (signal) => {
703
826
  clearInterval(refreshInterval);
704
827
  clearInterval(logCleanupInterval);
705
828
  logger.always(`\nShutting down proxy (${signal})...`);
829
+ // Flush and shutdown OpenTelemetry before closing the server
830
+ try {
831
+ const { flushOpenTelemetry, shutdownOpenTelemetry } = await import("../../lib/services/server/ai/observability/instrumentation.js");
832
+ await flushOpenTelemetry();
833
+ await shutdownOpenTelemetry();
834
+ }
835
+ catch {
836
+ /* non-fatal — proxy shutdown must not block on OTel */
837
+ }
706
838
  // Only clear Claude settings on user-initiated stop (SIGINT).
707
839
  // On SIGTERM (launchd restart cycle), leave settings intact so
708
840
  // the restarted proxy picks up seamlessly.
@@ -746,7 +878,10 @@ export const proxyStartCommand = {
746
878
  };
747
879
  function printStatusStats(stats) {
748
880
  console.info(`\n Stats:`);
749
- console.info(` Requests: ${stats.totalRequests} total, ${stats.totalSuccess} success, ${stats.totalErrors} errors`);
881
+ if (stats.totalAttempts !== undefined) {
882
+ console.info(` Attempts: ${stats.totalAttempts}`);
883
+ }
884
+ console.info(` Completed: ${stats.totalRequests} total, ${stats.totalSuccess} success, ${stats.totalErrors} errors`);
750
885
  console.info(` Rate limits: ${stats.totalRateLimits}`);
751
886
  if (stats.accounts?.length) {
752
887
  console.info(`\n Accounts:`);
@@ -754,7 +889,11 @@ function printStatusStats(stats) {
754
889
  const acctStatus = a.cooling
755
890
  ? chalk.red("cooling")
756
891
  : chalk.green("active");
757
- console.info(` ${a.label.padEnd(20)} ${a.type.padEnd(8)} ${String(a.requests).padEnd(6)} reqs ${acctStatus}`);
892
+ const attempts = a.attempts ?? a.requests ?? 0;
893
+ const success = a.success ?? 0;
894
+ const errors = a.errors ?? 0;
895
+ const rateLimits = a.rateLimits ?? 0;
896
+ console.info(` ${a.label.padEnd(20)} ${a.type.padEnd(8)} ${String(attempts).padEnd(6)} attempts ${String(success).padEnd(6)} success ${String(errors).padEnd(6)} errors ${String(rateLimits).padEnd(6)} rl ${acctStatus}`);
758
897
  }
759
898
  }
760
899
  }
@@ -793,6 +932,7 @@ export const proxyStatusCommand = {
793
932
  uptime: null,
794
933
  startTime: null,
795
934
  url: null,
935
+ envFile: null,
796
936
  fallbackChain: null,
797
937
  };
798
938
  if (state && isProcessRunning(state.pid)) {
@@ -804,10 +944,25 @@ export const proxyStatusCommand = {
804
944
  status.startTime = state.startTime;
805
945
  status.uptime = Date.now() - new Date(state.startTime).getTime();
806
946
  status.url = `http://${state.host === "0.0.0.0" ? "localhost" : state.host}:${state.port}`;
947
+ status.envFile = state.envFile ?? null;
807
948
  status.fallbackChain = state.fallbackChain ?? null;
808
949
  }
950
+ // Fetch live stats before rendering (JSON or text)
951
+ let liveStats = null;
952
+ if (status.running && status.url) {
953
+ try {
954
+ const statusResp = await fetch(`${status.url}/status`);
955
+ if (statusResp.ok) {
956
+ const statusData = (await statusResp.json());
957
+ liveStats = statusData.stats;
958
+ }
959
+ }
960
+ catch {
961
+ // Non-fatal — live stats unavailable
962
+ }
963
+ }
809
964
  if (argv.format === "json") {
810
- logger.always(JSON.stringify(status, null, 2));
965
+ logger.always(JSON.stringify({ ...status, stats: liveStats }, null, 2));
811
966
  return;
812
967
  }
813
968
  // Text format
@@ -822,6 +977,9 @@ export const proxyStatusCommand = {
822
977
  logger.always(` ${chalk.bold("Strategy:")} ${chalk.cyan(status.strategy)}`);
823
978
  logger.always(` ${chalk.bold("Started:")} ${chalk.cyan(status.startTime)}`);
824
979
  logger.always(` ${chalk.bold("Uptime:")} ${chalk.cyan(formatUptime(status.uptime ?? 0))}`);
980
+ if (status.envFile) {
981
+ logger.always(` ${chalk.bold("Env File:")} ${chalk.cyan(status.envFile)}`);
982
+ }
825
983
  // Display fallback chain if configured
826
984
  if (status.fallbackChain && status.fallbackChain.length > 0) {
827
985
  logger.always("");
@@ -875,6 +1033,58 @@ export const proxyStatusCommand = {
875
1033
  },
876
1034
  };
877
1035
  // =============================================================================
1036
+ // PROXY TELEMETRY COMMAND
1037
+ // =============================================================================
1038
+ const PROXY_TELEMETRY_ACTIONS = [
1039
+ "setup",
1040
+ "start",
1041
+ "stop",
1042
+ "status",
1043
+ "logs",
1044
+ "import-dashboard",
1045
+ ];
1046
+ export const proxyTelemetryCommand = {
1047
+ command: "telemetry <action>",
1048
+ describe: "Manage the local OpenObserve stack and dashboard for proxy observability",
1049
+ builder: (yargs) => yargs
1050
+ .positional("action", {
1051
+ type: "string",
1052
+ choices: [...PROXY_TELEMETRY_ACTIONS],
1053
+ describe: "Telemetry action: setup, start, stop, status, logs, or import-dashboard",
1054
+ })
1055
+ .option("quiet", {
1056
+ type: "boolean",
1057
+ alias: "q",
1058
+ default: false,
1059
+ description: "Suppress the local CLI spinner and delegate directly",
1060
+ })
1061
+ .example("neurolink proxy telemetry setup", "Start OpenObserve, start the OTEL collector, and import the dashboard")
1062
+ .example("neurolink proxy telemetry start", "Start the local proxy telemetry stack without re-importing the dashboard")
1063
+ .example("neurolink proxy telemetry stop", "Stop the local OpenObserve and OTEL collector containers"),
1064
+ handler: async (argv) => {
1065
+ const action = argv.action;
1066
+ const spinner = argv.quiet
1067
+ ? null
1068
+ : ora(`Running proxy telemetry ${action}...`).start();
1069
+ try {
1070
+ if (spinner) {
1071
+ spinner.stop();
1072
+ }
1073
+ await runProxyTelemetryManager(action);
1074
+ if (spinner) {
1075
+ spinner.succeed(`proxy telemetry ${action} completed`);
1076
+ }
1077
+ }
1078
+ catch (error) {
1079
+ if (spinner) {
1080
+ spinner.fail(`proxy telemetry ${action} failed`);
1081
+ }
1082
+ logger.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
1083
+ process.exit(1);
1084
+ }
1085
+ },
1086
+ };
1087
+ // =============================================================================
878
1088
  // PROXY FAIL-OPEN GUARD COMMAND (HIDDEN)
879
1089
  // =============================================================================
880
1090
  export const proxyGuardCommand = {
@@ -1152,6 +1362,11 @@ export const proxySetupCommand = {
1152
1362
  type: "boolean",
1153
1363
  default: false,
1154
1364
  description: "Skip service installation and start proxy in foreground instead",
1365
+ })
1366
+ .option("env-file", {
1367
+ type: "string",
1368
+ alias: "envFile",
1369
+ description: "Path to proxy provider env file to persist for the proxy",
1155
1370
  })
1156
1371
  .example("neurolink proxy setup", "Full setup with defaults")
1157
1372
  .example("neurolink proxy setup -p 9000", "Setup on custom port")
@@ -1253,9 +1468,27 @@ export const proxySetupCommand = {
1253
1468
  // =============================================================================
1254
1469
  // PROXY INSTALL / UNINSTALL — launchd service (macOS)
1255
1470
  // =============================================================================
1256
- function buildPlist(port, host) {
1257
- const nodeExec = process.execPath;
1258
- const entryScript = process.argv[1] ?? join(__dirname, "..", "index.js");
1471
+ function escapeXml(s) {
1472
+ return s
1473
+ .replace(/&/g, "&amp;")
1474
+ .replace(/</g, "&lt;")
1475
+ .replace(/>/g, "&gt;")
1476
+ .replace(/"/g, "&quot;")
1477
+ .replace(/'/g, "&apos;");
1478
+ }
1479
+ function buildPlist(port, host, envFile, configFile) {
1480
+ const nodeExec = escapeXml(process.execPath);
1481
+ const entryScript = escapeXml(process.argv[1] ?? join(__dirname, "..", "index.js"));
1482
+ const envFileArgs = envFile
1483
+ ? `
1484
+ <string>--env-file</string>
1485
+ <string>${escapeXml(envFile)}</string>`
1486
+ : "";
1487
+ const configArgs = configFile
1488
+ ? `
1489
+ <string>--config</string>
1490
+ <string>${escapeXml(configFile)}</string>`
1491
+ : "";
1259
1492
  return `<?xml version="1.0" encoding="UTF-8"?>
1260
1493
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
1261
1494
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -1274,6 +1507,8 @@ function buildPlist(port, host) {
1274
1507
  <string>${port}</string>
1275
1508
  <string>--host</string>
1276
1509
  <string>${host}</string>
1510
+ ${envFileArgs}
1511
+ ${configArgs}
1277
1512
  <string>--quiet</string>
1278
1513
  </array>
1279
1514
 
@@ -1320,6 +1555,15 @@ export const proxyInstallCommand = {
1320
1555
  type: "string",
1321
1556
  default: "127.0.0.1",
1322
1557
  description: "Proxy host",
1558
+ })
1559
+ .option("env-file", {
1560
+ type: "string",
1561
+ alias: "envFile",
1562
+ description: "Path to proxy provider env file to persist for the service",
1563
+ })
1564
+ .option("config", {
1565
+ type: "string",
1566
+ description: "Path to proxy routing config file to persist for the service",
1323
1567
  })
1324
1568
  .example("neurolink proxy install", "Install with defaults (port 55669)")
1325
1569
  .example("neurolink proxy install -p 9000", "Install on custom port");
@@ -1333,6 +1577,21 @@ export const proxyInstallCommand = {
1333
1577
  process.exit(1);
1334
1578
  }
1335
1579
  const { writeFileSync, mkdirSync, existsSync } = await import("fs");
1580
+ const envResolution = resolveProxyEnvFile({
1581
+ explicitEnvFile: argv.envFile,
1582
+ });
1583
+ const envFile = envResolution.path;
1584
+ const explicitConfig = argv.config;
1585
+ const configPath = explicitConfig ?? join(homedir(), ".neurolink", "proxy-config.yaml");
1586
+ if (explicitConfig && !existsSync(configPath)) {
1587
+ console.info(chalk.red(`Proxy config file not found: ${configPath}`));
1588
+ process.exit(1);
1589
+ }
1590
+ const configFile = existsSync(configPath) ? configPath : undefined;
1591
+ if (envFile && !existsSync(envFile)) {
1592
+ console.info(chalk.red(`Proxy env file not found: ${envFile}`));
1593
+ process.exit(1);
1594
+ }
1336
1595
  const logsDir = join(homedir(), ".neurolink", "logs");
1337
1596
  if (!existsSync(logsDir)) {
1338
1597
  mkdirSync(logsDir, { recursive: true });
@@ -1340,9 +1599,12 @@ export const proxyInstallCommand = {
1340
1599
  if (!existsSync(PLIST_DIR)) {
1341
1600
  mkdirSync(PLIST_DIR, { recursive: true });
1342
1601
  }
1343
- const plist = buildPlist(port, host);
1602
+ const plist = buildPlist(port, host, envFile, configFile);
1344
1603
  writeFileSync(PLIST_PATH, plist, "utf-8");
1345
1604
  console.info(chalk.green(`✓ Plist written to ${PLIST_PATH}`));
1605
+ if (envFile) {
1606
+ console.info(chalk.green(`✓ Proxy env file: ${envFile}`));
1607
+ }
1346
1608
  try {
1347
1609
  const { execFileSync } = await import("node:child_process");
1348
1610
  execFileSync("launchctl", ["unload", PLIST_PATH], {
@@ -1375,6 +1637,7 @@ export const proxyInstallCommand = {
1375
1637
  host,
1376
1638
  strategy: "fill-first",
1377
1639
  startTime: new Date().toISOString(),
1640
+ envFile,
1378
1641
  managedBy: "launchd",
1379
1642
  });
1380
1643
  }
@@ -341,6 +341,9 @@ export class TaskCommandFactory {
341
341
  schedule = { type: "interval", every: parseDuration(argv.every) };
342
342
  }
343
343
  else {
344
+ if (!argv.at) {
345
+ throw new Error("One-time tasks require --at");
346
+ }
344
347
  schedule = { type: "once", at: argv.at };
345
348
  }
346
349
  const now = new Date().toISOString();
@@ -10,6 +10,8 @@ export declare class CLICommandFactory {
10
10
  private static processCliPDFFiles;
11
11
  private static processCliFiles;
12
12
  private static processCliVideoFiles;
13
+ private static isNonLocalFileReference;
14
+ private static validateCliInputFiles;
13
15
  private static processOptions;
14
16
  /**
15
17
  * Validate Anthropic subscription options
@@ -480,6 +480,42 @@ export class CLICommandFactory {
480
480
  // URLs are preserved as-is by resolveFilePaths
481
481
  return resolveFilePaths(paths);
482
482
  }
483
+ static isNonLocalFileReference(filePath) {
484
+ const lower = filePath.toLowerCase();
485
+ return (lower.startsWith("http://") ||
486
+ lower.startsWith("https://") ||
487
+ lower.startsWith("file://") ||
488
+ lower.startsWith("data:"));
489
+ }
490
+ static validateCliInputFiles(argv) {
491
+ const fileArgs = [
492
+ { option: "--image", value: argv.image },
493
+ { option: "--csv", value: argv.csv },
494
+ { option: "--pdf", value: argv.pdf },
495
+ { option: "--video", value: argv.video },
496
+ { option: "--file", value: argv.file },
497
+ ];
498
+ const missingPaths = [];
499
+ for (const { option, value } of fileArgs) {
500
+ if (!value) {
501
+ continue;
502
+ }
503
+ const rawPaths = Array.isArray(value) ? value : [value];
504
+ const resolvedPaths = resolveFilePaths(rawPaths);
505
+ for (let i = 0; i < resolvedPaths.length; i++) {
506
+ const resolvedPath = resolvedPaths[i];
507
+ if (CLICommandFactory.isNonLocalFileReference(resolvedPath)) {
508
+ continue;
509
+ }
510
+ if (!fs.existsSync(resolvedPath)) {
511
+ missingPaths.push(`${option} path not found: ${rawPaths[i]} (resolved to ${resolvedPath})`);
512
+ }
513
+ }
514
+ }
515
+ if (missingPaths.length > 0) {
516
+ throw new Error(`One or more input files do not exist:\n${missingPaths.join("\n")}`);
517
+ }
518
+ }
483
519
  // Helper method to process common options
484
520
  static processOptions(argv) {
485
521
  // Handle noColor option by disabling chalk
@@ -1762,6 +1798,7 @@ export class CLICommandFactory {
1762
1798
  if (options.delay) {
1763
1799
  await new Promise((resolve) => setTimeout(resolve, options.delay));
1764
1800
  }
1801
+ CLICommandFactory.validateCliInputFiles(argv);
1765
1802
  // Process context
1766
1803
  const { inputText, contextMetadata } = CLICommandFactory.processGenerateContext(rawInput, options);
1767
1804
  // Handle dry-run mode for testing
@@ -2355,6 +2392,7 @@ export class CLICommandFactory {
2355
2392
  if (options.delay) {
2356
2393
  await new Promise((resolve) => setTimeout(resolve, options.delay));
2357
2394
  }
2395
+ CLICommandFactory.validateCliInputFiles(argv);
2358
2396
  const { inputText, contextMetadata } = await CLICommandFactory.processStreamContext(argv, options);
2359
2397
  // Handle dry-run mode for testing
2360
2398
  if (options.dryRun) {
@@ -13,7 +13,7 @@ import { ServeCommandFactory } from "./commands/serve.js";
13
13
  import { ragCommand } from "./commands/rag.js";
14
14
  import { ObservabilityCommandFactory } from "./commands/observability.js";
15
15
  import { TelemetryCommandFactory } from "./commands/telemetry.js";
16
- import { proxyStartCommand, proxyStatusCommand, proxySetupCommand, proxyGuardCommand, proxyInstallCommand, proxyUninstallCommand, } from "./commands/proxy.js";
16
+ import { proxyStartCommand, proxyStatusCommand, proxyTelemetryCommand, proxySetupCommand, proxyGuardCommand, proxyInstallCommand, proxyUninstallCommand, } from "./commands/proxy.js";
17
17
  import { EvaluateCommandFactory } from "./commands/evaluate.js";
18
18
  import { TaskCommandFactory } from "./commands/task.js";
19
19
  // Enhanced CLI with Professional UX
@@ -187,16 +187,17 @@ export function initializeCliParser() {
187
187
  .command(AuthCommandFactory.createAuthCommands())
188
188
  // Proxy Commands - Claude multi-account proxy
189
189
  .command({
190
- command: "proxy <subcommand>",
190
+ command: "proxy",
191
191
  describe: "Manage Claude multi-account proxy server",
192
192
  builder: (yargs) => yargs
193
193
  .command(proxyStartCommand)
194
194
  .command(proxyStatusCommand)
195
+ .command(proxyTelemetryCommand)
195
196
  .command(proxySetupCommand)
196
197
  .command(proxyGuardCommand)
197
198
  .command(proxyInstallCommand)
198
199
  .command(proxyUninstallCommand)
199
- .demandCommand(1, "Please specify a proxy subcommand: start, status, setup, guard, install, or uninstall"),
200
+ .demandCommand(1, "Please specify a proxy subcommand: start, status, telemetry <setup|start|stop|status|logs|import-dashboard>, setup, guard, install, or uninstall"),
200
201
  handler: () => { },
201
202
  })
202
203
  // Evaluate Command Group - Using EvaluateCommandFactory
@@ -186,6 +186,9 @@ export class NeuroLinkLanguageModel {
186
186
  // Drain anything already buffered.
187
187
  while (buffer.length > 0) {
188
188
  const chunk = buffer.shift();
189
+ if (!chunk) {
190
+ break;
191
+ }
189
192
  yield chunk;
190
193
  if (chunk.type === "finish") {
191
194
  return;