@juspay/neurolink 9.44.0 → 9.48.1

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 (78) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +230 -5
  3. package/dist/auth/tokenStore.js +4 -2
  4. package/dist/browser/neurolink.min.js +258 -258
  5. package/dist/cli/commands/authProviders.d.ts +1 -1
  6. package/dist/cli/commands/proxy.js +25 -2
  7. package/dist/cli/commands/task.js +1 -1
  8. package/dist/cli/errorHandler.js +1 -1
  9. package/dist/cli/factories/commandFactory.js +8 -2
  10. package/dist/cli/loop/conversationSelector.d.ts +2 -2
  11. package/dist/cli/loop/optionsSchema.d.ts +2 -2
  12. package/dist/cli/loop/session.d.ts +1 -1
  13. package/dist/cli/utils/audioFileUtils.d.ts +1 -1
  14. package/dist/cli/utils/envManager.d.ts +1 -1
  15. package/dist/cli/utils/videoFileUtils.d.ts +1 -1
  16. package/dist/client/auth.d.ts +3 -3
  17. package/dist/client/httpClient.d.ts +20 -20
  18. package/dist/client/index.d.ts +3 -3
  19. package/dist/client/interceptors.d.ts +1 -1
  20. package/dist/client/reactHooks.d.ts +1 -1
  21. package/dist/client/reactHooks.tsx +2 -2
  22. package/dist/client/sseClient.d.ts +1 -1
  23. package/dist/client/streamingClient.d.ts +1 -1
  24. package/dist/client/wsClient.d.ts +1 -1
  25. package/dist/files/fileTools.d.ts +1 -1
  26. package/dist/lib/agent/directTools.d.ts +2 -2
  27. package/dist/lib/auth/tokenStore.js +4 -2
  28. package/dist/lib/client/auth.d.ts +3 -3
  29. package/dist/lib/client/httpClient.d.ts +20 -20
  30. package/dist/lib/client/index.d.ts +3 -3
  31. package/dist/lib/client/interceptors.d.ts +1 -1
  32. package/dist/lib/client/reactHooks.d.ts +1 -1
  33. package/dist/lib/client/sseClient.d.ts +1 -1
  34. package/dist/lib/client/streamingClient.d.ts +1 -1
  35. package/dist/lib/client/wsClient.d.ts +1 -1
  36. package/dist/lib/files/fileTools.d.ts +1 -1
  37. package/dist/lib/rag/types.d.ts +1 -68
  38. package/dist/lib/server/types.d.ts +3 -847
  39. package/dist/lib/server/types.js +3 -64
  40. package/dist/lib/tasks/tools/taskTools.d.ts +1 -1
  41. package/dist/lib/types/analytics.d.ts +1 -1
  42. package/dist/lib/types/cli.d.ts +1 -1
  43. package/dist/lib/types/clientTypes.d.ts +38 -20
  44. package/dist/lib/types/configTypes.d.ts +1 -1
  45. package/dist/lib/types/configTypes.js +0 -1
  46. package/dist/lib/types/index.d.ts +9 -7
  47. package/dist/lib/types/index.js +5 -2
  48. package/dist/lib/types/ragTypes.d.ts +69 -0
  49. package/dist/lib/types/sdkTypes.d.ts +1 -2
  50. package/dist/lib/types/serverTypes.d.ts +858 -0
  51. package/dist/lib/types/serverTypes.js +68 -0
  52. package/dist/lib/types/streamTypes.d.ts +2 -2
  53. package/dist/lib/types/typeAliases.d.ts +1 -37
  54. package/dist/lib/utils/imageProcessor.d.ts +24 -1
  55. package/dist/lib/utils/imageProcessor.js +124 -8
  56. package/dist/lib/utils/messageBuilder.js +18 -6
  57. package/dist/lib/workflow/config.d.ts +3 -3
  58. package/dist/rag/errors/RAGError.d.ts +1 -1
  59. package/dist/rag/types.d.ts +1 -68
  60. package/dist/server/types.d.ts +3 -847
  61. package/dist/server/types.js +3 -64
  62. package/dist/types/analytics.d.ts +1 -1
  63. package/dist/types/cli.d.ts +1 -1
  64. package/dist/types/clientTypes.d.ts +38 -20
  65. package/dist/types/configTypes.d.ts +1 -1
  66. package/dist/types/index.d.ts +8 -6
  67. package/dist/types/index.js +5 -2
  68. package/dist/types/ragTypes.d.ts +69 -0
  69. package/dist/types/sdkTypes.d.ts +1 -2
  70. package/dist/types/serverTypes.d.ts +858 -0
  71. package/dist/types/serverTypes.js +67 -0
  72. package/dist/types/streamTypes.d.ts +2 -2
  73. package/dist/types/typeAliases.d.ts +1 -37
  74. package/dist/utils/imageProcessor.js +124 -8
  75. package/dist/utils/messageBuilder.js +18 -6
  76. package/dist/workflow/config.d.ts +3 -3
  77. package/package.json +1 -1
  78. package/scripts/observability/manage-local-openobserve.sh +30 -2
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Server Adapter Types
3
+ * Comprehensive type system for NeuroLink server adapters
4
+ */
5
+ // ============================================
6
+ // Error Types
7
+ // ============================================
8
+ /**
9
+ * Error categories for server adapter errors
10
+ */
11
+ export const ErrorCategory = {
12
+ CONFIG: "CONFIG",
13
+ VALIDATION: "VALIDATION",
14
+ EXECUTION: "EXECUTION",
15
+ EXTERNAL: "EXTERNAL",
16
+ RATE_LIMIT: "RATE_LIMIT",
17
+ AUTHENTICATION: "AUTHENTICATION",
18
+ AUTHORIZATION: "AUTHORIZATION",
19
+ STREAMING: "STREAMING",
20
+ WEBSOCKET: "WEBSOCKET",
21
+ };
22
+ /**
23
+ * Error severity levels
24
+ */
25
+ export const ErrorSeverity = {
26
+ LOW: "LOW",
27
+ MEDIUM: "MEDIUM",
28
+ HIGH: "HIGH",
29
+ CRITICAL: "CRITICAL",
30
+ };
31
+ /**
32
+ * Server adapter error codes
33
+ */
34
+ export const ServerAdapterErrorCode = {
35
+ // Configuration errors
36
+ INVALID_CONFIG: "SERVER_ADAPTER_INVALID_CONFIG",
37
+ MISSING_DEPENDENCY: "SERVER_ADAPTER_MISSING_DEPENDENCY",
38
+ FRAMEWORK_INIT_FAILED: "SERVER_ADAPTER_FRAMEWORK_INIT_FAILED",
39
+ // Route errors
40
+ ROUTE_NOT_FOUND: "SERVER_ADAPTER_ROUTE_NOT_FOUND",
41
+ ROUTE_CONFLICT: "SERVER_ADAPTER_ROUTE_CONFLICT",
42
+ INVALID_ROUTE: "SERVER_ADAPTER_INVALID_ROUTE",
43
+ // Execution errors
44
+ HANDLER_ERROR: "SERVER_ADAPTER_HANDLER_ERROR",
45
+ TIMEOUT: "SERVER_ADAPTER_TIMEOUT",
46
+ MIDDLEWARE_ERROR: "SERVER_ADAPTER_MIDDLEWARE_ERROR",
47
+ // Rate limit errors
48
+ RATE_LIMIT_EXCEEDED: "SERVER_ADAPTER_RATE_LIMIT_EXCEEDED",
49
+ // Authentication/Authorization errors
50
+ AUTH_REQUIRED: "SERVER_ADAPTER_AUTH_REQUIRED",
51
+ AUTH_INVALID: "SERVER_ADAPTER_AUTH_INVALID",
52
+ FORBIDDEN: "SERVER_ADAPTER_FORBIDDEN",
53
+ // Streaming errors
54
+ STREAM_ERROR: "SERVER_ADAPTER_STREAM_ERROR",
55
+ STREAM_ABORTED: "SERVER_ADAPTER_STREAM_ABORTED",
56
+ // WebSocket errors
57
+ WEBSOCKET_ERROR: "SERVER_ADAPTER_WEBSOCKET_ERROR",
58
+ WEBSOCKET_CONNECTION_FAILED: "SERVER_ADAPTER_WEBSOCKET_CONNECTION_FAILED",
59
+ // Validation errors
60
+ VALIDATION_ERROR: "SERVER_ADAPTER_VALIDATION_ERROR",
61
+ SCHEMA_ERROR: "SERVER_ADAPTER_SCHEMA_ERROR",
62
+ // Server lifecycle errors
63
+ START_FAILED: "SERVER_ADAPTER_START_FAILED",
64
+ STOP_FAILED: "SERVER_ADAPTER_STOP_FAILED",
65
+ ALREADY_RUNNING: "SERVER_ADAPTER_ALREADY_RUNNING",
66
+ NOT_RUNNING: "SERVER_ADAPTER_NOT_RUNNING",
67
+ };
@@ -1,7 +1,7 @@
1
1
  import type { LanguageModel, StepResult, Tool, ToolChoice } from "ai";
2
2
  import type { AIProviderName } from "../constants/enums.js";
3
- import type { EvaluationData } from "../index.js";
4
- import type { RAGConfig } from "../rag/types.js";
3
+ import type { EvaluationData } from "./evaluation.js";
4
+ import type { RAGConfig } from "./ragTypes.js";
5
5
  import type { AnalyticsData, ToolExecutionEvent, ToolExecutionSummary } from "../types/index.js";
6
6
  import type { MiddlewareFactoryOptions, OnChunkCallback, OnErrorCallback, OnFinishCallback } from "../types/middlewareTypes.js";
7
7
  import type { TokenUsage } from "./analytics.js";
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { ZodTypeAny } from "zod";
6
6
  import type { Schema } from "ai";
7
- import type { JsonValue, JsonObject } from "./common.js";
7
+ import type { JsonValue, Result, AsyncFunction } from "./common.js";
8
8
  import { zodToJsonSchema } from "zod-to-json-schema";
9
9
  /**
10
10
  * Type alias for complex Zod schema type to improve readability
@@ -65,11 +65,6 @@ export type JsonRecord = Record<string, JsonValue>;
65
65
  export type OptionalStandardRecord = StandardRecord | undefined;
66
66
  export type OptionalStringRecord = StringRecord | undefined;
67
67
  export type OptionalJsonRecord = JsonRecord | undefined;
68
- /**
69
- * Standard async function type for tool execution
70
- * Most common function signature in the codebase
71
- */
72
- export type AsyncFunction<TParams = unknown, TResult = unknown> = (params: TParams) => Promise<TResult>;
73
68
  /**
74
69
  * Tool execution function with context
75
70
  * Standard pattern for MCP tool execution
@@ -98,21 +93,11 @@ export type TransformFunction<TInput = unknown, TOutput = unknown> = (input: TIn
98
93
  * Async transformation function type
99
94
  */
100
95
  export type AsyncTransformFunction<TInput = unknown, TOutput = unknown> = (input: TInput) => Promise<TOutput>;
101
- /**
102
- * Array of unknown values
103
- * Common for flexible array parameters
104
- */
105
- export type UnknownArray = unknown[];
106
96
  /**
107
97
  * Array of standard records
108
98
  * Common in data collections
109
99
  */
110
100
  export type RecordArray = StandardRecord[];
111
- /**
112
- * Array of JSON objects
113
- * API-safe array type
114
- */
115
- export type JsonArray = JsonObject[];
116
101
  /**
117
102
  * String array type
118
103
  * Very common for lists of identifiers, names, etc.
@@ -203,17 +188,6 @@ export type StandardError = {
203
188
  details?: StandardRecord;
204
189
  stack?: string;
205
190
  };
206
- /**
207
- * Result type with success/error pattern
208
- * Common pattern for operation results
209
- */
210
- export type Result<TData = unknown, TError = StandardError> = {
211
- success: true;
212
- data: TData;
213
- } | {
214
- success: false;
215
- error: TError;
216
- };
217
191
  /**
218
192
  * Async result type
219
193
  */
@@ -285,16 +259,6 @@ export type ServiceConfig = {
285
259
  apiKey?: string;
286
260
  metadata?: StandardRecord;
287
261
  };
288
- /**
289
- * Cache configuration
290
- * Common caching parameters
291
- */
292
- export type CacheConfig = {
293
- enabled?: boolean;
294
- ttl?: number;
295
- maxSize?: number;
296
- strategy?: "memory" | "redis" | "hybrid";
297
- };
298
262
  /**
299
263
  * Rate limiting configuration
300
264
  * Common rate limiting parameters
@@ -364,11 +364,58 @@ export class ImageProcessor {
364
364
  const height = buffer.readUInt32BE(20);
365
365
  return { width, height };
366
366
  }
367
- // Basic JPEG dimension extraction (simplified)
367
+ // JPEG dimension extraction via SOF marker parsing
368
368
  if (buffer.length >= 4 && buffer[0] === 0xff && buffer[1] === 0xd8) {
369
- // This is a very basic implementation
370
- // For production, consider using a proper image library
371
- return null;
369
+ // Search for SOF0 (0xFFC0) or SOF2 (0xFFC2) markers
370
+ let offset = 2;
371
+ while (offset < buffer.length - 1) {
372
+ // Find next marker (0xFF followed by non-zero, non-0xFF byte)
373
+ if (buffer[offset] !== 0xff) {
374
+ offset++;
375
+ continue;
376
+ }
377
+ // Skip any padding 0xFF bytes
378
+ while (offset < buffer.length && buffer[offset] === 0xff) {
379
+ offset++;
380
+ }
381
+ if (offset >= buffer.length) {
382
+ break;
383
+ }
384
+ const marker = buffer[offset];
385
+ offset++;
386
+ // Check for SOF0 (0xC0 - baseline DCT) and SOF2 (0xC2 - progressive DCT)
387
+ // These are the most common JPEG encoding modes
388
+ if (marker === 0xc0 || marker === 0xc2) {
389
+ // SOF marker found: length (2 bytes) + precision (1 byte) + height (2 bytes) + width (2 bytes)
390
+ if (offset + 7 > buffer.length) {
391
+ break; // Truncated file
392
+ }
393
+ const height = buffer.readUInt16BE(offset + 3);
394
+ const width = buffer.readUInt16BE(offset + 5);
395
+ return { width, height };
396
+ }
397
+ // Skip this marker's segment (except for markers without length)
398
+ if (marker === 0xd0 ||
399
+ marker === 0xd1 ||
400
+ marker === 0xd2 ||
401
+ marker === 0xd3 ||
402
+ marker === 0xd4 ||
403
+ marker === 0xd5 ||
404
+ marker === 0xd6 ||
405
+ marker === 0xd7 ||
406
+ marker === 0xd8 ||
407
+ marker === 0xd9 ||
408
+ marker === 0x01) {
409
+ // RST0-RST7, SOI, EOI, TEM - no length field
410
+ continue;
411
+ }
412
+ if (offset + 2 > buffer.length) {
413
+ break; // Truncated file
414
+ }
415
+ const segmentLength = buffer.readUInt16BE(offset);
416
+ offset += segmentLength;
417
+ }
418
+ return null; // No SOF marker found
372
419
  }
373
420
  return null;
374
421
  }
@@ -437,6 +484,30 @@ export class ImageProcessor {
437
484
  }
438
485
  }
439
486
  }
487
+ /**
488
+ * Whitelist of valid image file extensions (lowercase, no dots).
489
+ * Used to validate file extensions against a known set of image formats.
490
+ */
491
+ export const VALID_IMAGE_EXTENSIONS = [
492
+ "jpg",
493
+ "jpeg",
494
+ "png",
495
+ "gif",
496
+ "webp",
497
+ "bmp",
498
+ "tiff",
499
+ "tif",
500
+ "svg",
501
+ "avif",
502
+ "ico",
503
+ "heic",
504
+ "heif",
505
+ ];
506
+ /**
507
+ * Set of valid image extensions for O(1) lookup.
508
+ * @internal
509
+ */
510
+ const VALID_IMAGE_EXTENSIONS_SET = new Set(VALID_IMAGE_EXTENSIONS);
440
511
  /**
441
512
  * Utility functions for image handling
442
513
  */
@@ -466,11 +537,52 @@ export const imageUtils = {
466
537
  */
467
538
  isBase64: (str) => imageUtils.isValidBase64(str),
468
539
  /**
469
- * Extract file extension from filename or URL
540
+ * Extract file extension from filename or URL.
541
+ * Strips query strings and fragments before matching so that
542
+ * "image.jpg?v=1" correctly returns "jpg".
543
+ * Returns null if no extension is found or if the extension
544
+ * contains non-alphanumeric characters.
470
545
  */
471
546
  getFileExtension: (filename) => {
472
- const match = filename.match(/\.([^.]+)$/);
473
- return match ? match[1].toLowerCase() : null;
547
+ const sanitized = filename.split(/[?#]/)[0];
548
+ const match = sanitized.match(/\.([^.]+)$/);
549
+ if (!match) {
550
+ return null;
551
+ }
552
+ const extension = match[1].toLowerCase();
553
+ if (!/^[a-z0-9]+$/.test(extension)) {
554
+ return null;
555
+ }
556
+ return extension;
557
+ },
558
+ /**
559
+ * Validate that an extension is a recognised image format.
560
+ * Case-insensitive; rejects extensions with special characters.
561
+ */
562
+ isValidImageExtension: (extension) => {
563
+ if (!extension || typeof extension !== "string") {
564
+ return false;
565
+ }
566
+ const normalizedExt = extension.toLowerCase();
567
+ if (!/^[a-z0-9]+$/.test(normalizedExt)) {
568
+ return false;
569
+ }
570
+ return VALID_IMAGE_EXTENSIONS_SET.has(normalizedExt);
571
+ },
572
+ /**
573
+ * Extract and validate image file extension from a filename or URL.
574
+ * Returns null if the extension is missing or not a recognised image format.
575
+ *
576
+ * Security note: the last extension is used, so "malware.exe.jpg" returns
577
+ * "jpg". Callers should apply additional checks (e.g. content inspection)
578
+ * where double-extension attacks are a concern.
579
+ */
580
+ getValidatedImageExtension: (filename) => {
581
+ const extension = imageUtils.getFileExtension(filename);
582
+ if (!extension) {
583
+ return null;
584
+ }
585
+ return imageUtils.isValidImageExtension(extension) ? extension : null;
474
586
  },
475
587
  /**
476
588
  * Convert file size to human readable format
@@ -576,7 +688,11 @@ export const imageUtils = {
576
688
  if (!/^image\//i.test(contentType)) {
577
689
  throw new Error(`Unsupported content-type: ${contentType || "unknown"}`);
578
690
  }
579
- const len = Number(response.headers.get("content-length") || 0);
691
+ const contentLengthHeader = response.headers.get("content-length");
692
+ const len = Number(contentLengthHeader || 0);
693
+ if (contentLengthHeader !== null && len === 0) {
694
+ throw new Error("Empty response: content-length is 0");
695
+ }
580
696
  if (len && len > maxBytes) {
581
697
  throw new Error(`Content too large: ${len} bytes`);
582
698
  }
@@ -1127,15 +1127,27 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1127
1127
  async function convertContentToProviderFormat(content, provider, _model) {
1128
1128
  const textContent = content.find((c) => c.type === "text");
1129
1129
  const imageContent = content.filter((c) => c.type === "image");
1130
- if (!textContent) {
1131
- throw new Error("Multimodal content must include at least one text element");
1132
- }
1133
- if (imageContent.length === 0) {
1134
- return textContent.text;
1130
+ const pdfContent = content.filter((c) => c.type === "pdf");
1131
+ // Allow empty text when multimodal content is present (enables image-only or PDF-only queries)
1132
+ const text = textContent?.text || "";
1133
+ const hasMultimodal = imageContent.length > 0 || pdfContent.length > 0;
1134
+ // Validate that we have at least some content
1135
+ if (!hasMultimodal && !text) {
1136
+ throw new Error("Content must include either text or multimodal content");
1137
+ }
1138
+ // Text-only case
1139
+ if (imageContent.length === 0 && pdfContent.length === 0) {
1140
+ return text;
1135
1141
  }
1136
1142
  // Extract images as Buffer | string array
1137
1143
  const images = imageContent.map((img) => img.data);
1138
- return await convertSimpleImagesToProviderFormat(textContent.text, images, provider, _model);
1144
+ // Extract PDFs in the expected format
1145
+ const pdfFiles = pdfContent.map((pdf) => ({
1146
+ buffer: typeof pdf.data === "string" ? Buffer.from(pdf.data, "base64") : pdf.data,
1147
+ filename: pdf.metadata?.filename || "document.pdf",
1148
+ pageCount: pdf.metadata?.pages ?? null,
1149
+ }));
1150
+ return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, _model);
1139
1151
  }
1140
1152
  /**
1141
1153
  * Check if a string is an internet URL
@@ -41,8 +41,8 @@ export declare const JudgeConfigSchema: z.ZodObject<{
41
41
  model: z.ZodString;
42
42
  criteria: z.ZodArray<z.ZodString>;
43
43
  outputFormat: z.ZodEnum<{
44
- scores: "scores";
45
44
  detailed: "detailed";
45
+ scores: "scores";
46
46
  ranking: "ranking";
47
47
  best: "best";
48
48
  }>;
@@ -195,8 +195,8 @@ export declare const WorkflowConfigSchema: z.ZodObject<{
195
195
  model: z.ZodString;
196
196
  criteria: z.ZodArray<z.ZodString>;
197
197
  outputFormat: z.ZodEnum<{
198
- scores: "scores";
199
198
  detailed: "detailed";
199
+ scores: "scores";
200
200
  ranking: "ranking";
201
201
  best: "best";
202
202
  }>;
@@ -219,8 +219,8 @@ export declare const WorkflowConfigSchema: z.ZodObject<{
219
219
  model: z.ZodString;
220
220
  criteria: z.ZodArray<z.ZodString>;
221
221
  outputFormat: z.ZodEnum<{
222
- scores: "scores";
223
222
  detailed: "detailed";
223
+ scores: "scores";
224
224
  ranking: "ranking";
225
225
  best: "best";
226
226
  }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.44.0",
3
+ "version": "9.48.1",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
6
6
  "author": {
@@ -167,6 +167,33 @@ export NEUROLINK_OPENOBSERVE_OTLP_ENDPOINT
167
167
 
168
168
  select_compose
169
169
 
170
+ # Write or update OTEL_EXPORTER_OTLP_ENDPOINT in ~/.neurolink/.env so the
171
+ # proxy picks it up automatically without any manual export.
172
+ upsert_neurolink_env() {
173
+ local neurolink_dir="${HOME}/.neurolink"
174
+ local env_file="${neurolink_dir}/.env"
175
+ local endpoint="http://localhost:${NEUROLINK_OTLP_HTTP_PORT}"
176
+ local key="OTEL_EXPORTER_OTLP_ENDPOINT"
177
+
178
+ mkdir -p "${neurolink_dir}"
179
+ chmod 700 "${neurolink_dir}" 2>/dev/null || true
180
+
181
+ if [[ -f "${env_file}" ]] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?${key}=" "${env_file}"; then
182
+ # Replace existing line in-place (portable sed -i)
183
+ sed -i.bak -E "s|^[[:space:]]*(export[[:space:]]+)?${key}=.*|${key}=${endpoint}|" "${env_file}"
184
+ rm -f "${env_file}.bak"
185
+ else
186
+ # Ensure the new entry starts on its own line if the file lacks a trailing newline
187
+ if [[ -f "${env_file}" ]] && [[ -s "${env_file}" ]] && [[ "$(tail -c 1 "${env_file}")" != "" ]]; then
188
+ echo "" >> "${env_file}"
189
+ fi
190
+ echo "${key}=${endpoint}" >> "${env_file}"
191
+ fi
192
+ chmod 600 "${env_file}" 2>/dev/null || true
193
+
194
+ echo " Wrote ${key}=${endpoint} to ${env_file}"
195
+ }
196
+
170
197
  command="${1:-setup}"
171
198
 
172
199
  case "${command}" in
@@ -175,6 +202,7 @@ case "${command}" in
175
202
  wait_for_http "OpenObserve" "${NEUROLINK_OPENOBSERVE_URL}/healthz"
176
203
  wait_for_http "OTEL collector" "http://localhost:${NEUROLINK_OTEL_HEALTH_PORT}/"
177
204
  node "${IMPORT_SCRIPT}" --replace-by-title
205
+ upsert_neurolink_env
178
206
  cat <<EOF
179
207
  Local proxy observability is ready.
180
208
 
@@ -183,12 +211,12 @@ OpenObserve login user: ${NEUROLINK_OPENOBSERVE_USER}
183
211
  OpenObserve password source: ${ENV_FILE} (NEUROLINK_OPENOBSERVE_PASSWORD)
184
212
  OTLP HTTP endpoint: http://localhost:${NEUROLINK_OTLP_HTTP_PORT}
185
213
 
186
- Set this before starting the proxy:
187
- export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:${NEUROLINK_OTLP_HTTP_PORT}
214
+ OTEL endpoint written to ~/.neurolink/.env — the proxy will pick it up automatically on next start.
188
215
  EOF
189
216
  ;;
190
217
  up|start)
191
218
  compose up -d
219
+ upsert_neurolink_env
192
220
  ;;
193
221
  down|stop)
194
222
  compose down