@juspay/neurolink 9.50.1 → 9.51.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 (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +282 -282
  3. package/dist/cli/commands/proxy.js +60 -15
  4. package/dist/cli/utils/serverUtils.d.ts +2 -1
  5. package/dist/cli/utils/serverUtils.js +7 -3
  6. package/dist/context/contextCompactor.js +2 -2
  7. package/dist/context/stages/slidingWindowTruncator.d.ts +1 -1
  8. package/dist/context/stages/slidingWindowTruncator.js +3 -3
  9. package/dist/core/modules/Utilities.d.ts +5 -0
  10. package/dist/core/modules/Utilities.js +29 -18
  11. package/dist/lib/context/contextCompactor.js +2 -2
  12. package/dist/lib/context/stages/slidingWindowTruncator.d.ts +1 -1
  13. package/dist/lib/context/stages/slidingWindowTruncator.js +3 -3
  14. package/dist/lib/core/modules/Utilities.d.ts +5 -0
  15. package/dist/lib/core/modules/Utilities.js +29 -18
  16. package/dist/lib/mcp/externalServerManager.d.ts +5 -0
  17. package/dist/lib/mcp/externalServerManager.js +24 -2
  18. package/dist/lib/neurolink.js +37 -3
  19. package/dist/lib/proxy/accountQuota.d.ts +6 -0
  20. package/dist/lib/proxy/accountQuota.js +24 -3
  21. package/dist/lib/proxy/proxyPaths.d.ts +25 -0
  22. package/dist/lib/proxy/proxyPaths.js +35 -0
  23. package/dist/lib/proxy/requestLogger.d.ts +1 -1
  24. package/dist/lib/proxy/requestLogger.js +2 -2
  25. package/dist/lib/services/server/ai/observability/instrumentation.js +39 -1
  26. package/dist/lib/types/cli.d.ts +1 -0
  27. package/dist/lib/types/externalMcp.d.ts +7 -0
  28. package/dist/mcp/externalServerManager.d.ts +5 -0
  29. package/dist/mcp/externalServerManager.js +24 -2
  30. package/dist/neurolink.js +37 -3
  31. package/dist/proxy/accountQuota.d.ts +6 -0
  32. package/dist/proxy/accountQuota.js +24 -3
  33. package/dist/proxy/proxyPaths.d.ts +25 -0
  34. package/dist/proxy/proxyPaths.js +34 -0
  35. package/dist/proxy/requestLogger.d.ts +1 -1
  36. package/dist/proxy/requestLogger.js +2 -2
  37. package/dist/services/server/ai/observability/instrumentation.js +39 -1
  38. package/dist/types/cli.d.ts +1 -0
  39. package/dist/types/externalMcp.d.ts +7 -0
  40. package/package.json +1 -1
@@ -16,6 +16,12 @@ import type { AccountQuota } from "../types/index.js";
16
16
  * Pure computation — no I/O, no blocking.
17
17
  */
18
18
  export declare function parseQuotaHeaders(headers: Headers | Record<string, string>): AccountQuota | null;
19
+ /**
20
+ * Initialise the quota module with a custom file path.
21
+ * When set, all reads/writes go to this path instead of the default
22
+ * ~/.neurolink/account-quotas.json. Call before the first load/save.
23
+ */
24
+ export declare function initAccountQuota(quotaFilePath: string): void;
19
25
  /**
20
26
  * Load all persisted account quotas.
21
27
  * First call reads from disk; subsequent calls return the in-memory cache.
@@ -9,7 +9,7 @@
9
9
  * updates an in-memory cache and debounces disk writes so the request/response
10
10
  * path is never blocked by file I/O.
11
11
  */
12
- import { join } from "path";
12
+ import { dirname, join } from "path";
13
13
  import { homedir } from "os";
14
14
  import { promises as fs } from "fs";
15
15
  // ---------------------------------------------------------------------------
@@ -73,11 +73,32 @@ let memoryCache = {};
73
73
  let cacheLoaded = false;
74
74
  let dirty = false;
75
75
  let flushTimer = null;
76
+ /** Custom quota file path set via initAccountQuota(). */
77
+ let customQuotaFilePath = null;
78
+ /**
79
+ * Initialise the quota module with a custom file path.
80
+ * When set, all reads/writes go to this path instead of the default
81
+ * ~/.neurolink/account-quotas.json. Call before the first load/save.
82
+ */
83
+ export function initAccountQuota(quotaFilePath) {
84
+ customQuotaFilePath = quotaFilePath;
85
+ // Cancel any pending flush from a previous configuration so it does not
86
+ // write stale data to the new path.
87
+ if (flushTimer) {
88
+ clearTimeout(flushTimer);
89
+ flushTimer = null;
90
+ }
91
+ // Reset cache so the new path is picked up on next load
92
+ memoryCache = {};
93
+ cacheLoaded = false;
94
+ dirty = false;
95
+ }
76
96
  function getQuotaFilePath() {
77
- return join(homedir(), ".neurolink", QUOTA_FILE);
97
+ return customQuotaFilePath ?? join(homedir(), ".neurolink", QUOTA_FILE);
78
98
  }
79
99
  async function ensureDir() {
80
- const dir = join(homedir(), ".neurolink");
100
+ const filePath = getQuotaFilePath();
101
+ const dir = dirname(filePath);
81
102
  await fs.mkdir(dir, { recursive: true, mode: 0o700 }).catch(() => {
82
103
  // Non-fatal: directory may already exist
83
104
  });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Proxy file path resolver.
3
+ *
4
+ * In normal mode, all paths resolve under ~/.neurolink/.
5
+ * In dev mode (--dev), writable paths resolve under <cwd>/.neurolink-dev/
6
+ * so a local dev proxy never touches the global proxy's state.
7
+ *
8
+ * Read-only paths (like .env) always point to the global location
9
+ * since credentials must be shared.
10
+ *
11
+ * NOTE: Claude Code header snapshots (~/.neurolink/header-snapshots/) are
12
+ * not redirected in dev mode. They are only written when a real Claude Code
13
+ * client connects, which typically does not happen during dev testing.
14
+ */
15
+ export type ProxyPaths = {
16
+ /** Base directory for proxy state files */
17
+ stateDir: string;
18
+ /** logs/ — request/response logs */
19
+ logsDir: string;
20
+ /** account-quotas.json — per-account rate limit state */
21
+ quotaFile: string;
22
+ /** Whether this is a dev-mode isolated instance */
23
+ isDev: boolean;
24
+ };
25
+ export declare function resolveProxyPaths(dev: boolean): ProxyPaths;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Proxy file path resolver.
3
+ *
4
+ * In normal mode, all paths resolve under ~/.neurolink/.
5
+ * In dev mode (--dev), writable paths resolve under <cwd>/.neurolink-dev/
6
+ * so a local dev proxy never touches the global proxy's state.
7
+ *
8
+ * Read-only paths (like .env) always point to the global location
9
+ * since credentials must be shared.
10
+ *
11
+ * NOTE: Claude Code header snapshots (~/.neurolink/header-snapshots/) are
12
+ * not redirected in dev mode. They are only written when a real Claude Code
13
+ * client connects, which typically does not happen during dev testing.
14
+ */
15
+ import { homedir } from "node:os";
16
+ import { join } from "node:path";
17
+ export function resolveProxyPaths(dev) {
18
+ if (dev) {
19
+ const base = join(process.cwd(), ".neurolink-dev");
20
+ return {
21
+ stateDir: base,
22
+ logsDir: join(base, "logs"),
23
+ quotaFile: join(base, "account-quotas.json"),
24
+ isDev: true,
25
+ };
26
+ }
27
+ const base = join(homedir(), ".neurolink");
28
+ return {
29
+ stateDir: base,
30
+ logsDir: join(base, "logs"),
31
+ quotaFile: join(base, "account-quotas.json"),
32
+ isDev: false,
33
+ };
34
+ }
35
+ //# sourceMappingURL=proxyPaths.js.map
@@ -6,7 +6,7 @@
6
6
  * Useful for debugging and auditing proxy traffic.
7
7
  */
8
8
  import type { RequestAttemptLogEntry, RequestLogEntry } from "../types/index.js";
9
- export declare function initRequestLogger(enabled?: boolean): void;
9
+ export declare function initRequestLogger(enabled?: boolean, customLogsDir?: string): void;
10
10
  export declare function logRequest(entry: RequestLogEntry): Promise<void>;
11
11
  /**
12
12
  * Log an upstream attempt separately from the final request outcome.
@@ -44,13 +44,13 @@ const SENSITIVE_HEADER_NAMES = new Set([
44
44
  const SENSITIVE_HEADER_PATTERN = /token|secret|key|password|credential/i;
45
45
  /** JSON keys whose values should be redacted in request/response bodies. */
46
46
  const SENSITIVE_BODY_KEYS = /("(?:password|access_token|refresh_token|api_key|apiKey|secret|authorization|token|credential|x-api-key)"\s*:\s*)"(?:[^"\\]|\\.)*"/gi;
47
- export function initRequestLogger(enabled = true) {
47
+ export function initRequestLogger(enabled = true, customLogsDir) {
48
48
  logEnabled = enabled;
49
49
  if (!enabled) {
50
50
  return;
51
51
  }
52
52
  try {
53
- logDir = join(homedir(), ".neurolink", "logs");
53
+ logDir = customLogsDir ?? join(homedir(), ".neurolink", "logs");
54
54
  if (!existsSync(logDir)) {
55
55
  mkdirSync(logDir, { recursive: true, mode: 0o700 });
56
56
  }
@@ -445,7 +445,45 @@ function initializeExternalOpenTelemetryMode(config, resource, otlpEndpoint, ser
445
445
  const provider = globalProvider;
446
446
  if (globalProvider && typeof provider.addSpanProcessor === "function") {
447
447
  provider.addSpanProcessor(new ContextEnricher());
448
- const skipLangfuse = config.skipLangfuseSpanProcessor === true || !langfuseProcessor;
448
+ // Auto-detect: skip if consumer already registered a LangfuseSpanProcessor.
449
+ //
450
+ // Detection strategy (ordered by robustness):
451
+ // 1. `instanceof LangfuseSpanProcessor` — reliable when both sides use
452
+ // the same @langfuse/otel package instance (same module identity).
453
+ // 2. Duck-type check for Langfuse-specific public member
454
+ // (`langfuseClient` property) — survives minification.
455
+ // 3. `constructor.name === "LangfuseSpanProcessor"` — last resort,
456
+ // brittle under minification or bundler renaming.
457
+ //
458
+ // NOTE: `_registeredSpanProcessors` is an internal OpenTelemetry field.
459
+ // If the OTel SDK removes or renames it, the array defaults to [] and
460
+ // `hasExistingLangfuse` is false — NeuroLink registers its own processor
461
+ // (same behavior as before this check). Consumers can always force skip
462
+ // via `skipLangfuseSpanProcessor: true`.
463
+ const existingProcessors = provider
464
+ ._registeredSpanProcessors ?? [];
465
+ const hasExistingLangfuse = existingProcessors.some((p) => {
466
+ if (p === null || p === undefined || typeof p !== "object") {
467
+ return false;
468
+ }
469
+ // Prefer instanceof — works when same @langfuse/otel package is shared
470
+ if (p instanceof LangfuseSpanProcessor) {
471
+ return true;
472
+ }
473
+ // Duck-type: Langfuse processor exposes a langfuseClient property
474
+ if ("langfuseClient" in p) {
475
+ return true;
476
+ }
477
+ // Fallback: constructor name (brittle under minification)
478
+ return (p.constructor?.name ===
479
+ "LangfuseSpanProcessor");
480
+ });
481
+ const skipLangfuse = config.skipLangfuseSpanProcessor === true ||
482
+ !langfuseProcessor ||
483
+ hasExistingLangfuse;
484
+ if (hasExistingLangfuse && !config.skipLangfuseSpanProcessor) {
485
+ logger.info(`${LOG_PREFIX} Auto-detected existing LangfuseSpanProcessor — skipping SDK registration to avoid duplicates`);
486
+ }
449
487
  if (!skipLangfuse && langfuseProcessor) {
450
488
  provider.addSpanProcessor(langfuseProcessor);
451
489
  }
@@ -765,6 +765,7 @@ export type ProxyStartArgs = {
765
765
  config?: string;
766
766
  envFile?: string;
767
767
  passthrough?: boolean;
768
+ dev?: boolean;
768
769
  };
769
770
  /** Arguments accepted by `neurolink proxy status` */
770
771
  export type ProxyStatusArgs = {
@@ -228,6 +228,7 @@ export type ExternalMCPServerEvents = {
228
228
  /** Server status changed */
229
229
  statusChanged: {
230
230
  serverId: string;
231
+ serverName: string;
231
232
  oldStatus: ExternalMCPServerStatus;
232
233
  newStatus: ExternalMCPServerStatus;
233
234
  timestamp: Date;
@@ -235,24 +236,28 @@ export type ExternalMCPServerEvents = {
235
236
  /** Server connected successfully */
236
237
  connected: {
237
238
  serverId: string;
239
+ serverName: string;
238
240
  toolCount: number;
239
241
  timestamp: Date;
240
242
  };
241
243
  /** Server disconnected */
242
244
  disconnected: {
243
245
  serverId: string;
246
+ serverName: string;
244
247
  reason?: string;
245
248
  timestamp: Date;
246
249
  };
247
250
  /** Server failed */
248
251
  failed: {
249
252
  serverId: string;
253
+ serverName: string;
250
254
  error: string;
251
255
  timestamp: Date;
252
256
  };
253
257
  /** Tool discovered */
254
258
  toolDiscovered: {
255
259
  serverId: string;
260
+ serverName: string;
256
261
  toolName: string;
257
262
  toolInfo: ExternalMCPToolInfo;
258
263
  timestamp: Date;
@@ -260,12 +265,14 @@ export type ExternalMCPServerEvents = {
260
265
  /** Tool removed */
261
266
  toolRemoved: {
262
267
  serverId: string;
268
+ serverName: string;
263
269
  toolName: string;
264
270
  timestamp: Date;
265
271
  };
266
272
  /** Health check completed */
267
273
  healthCheck: {
268
274
  serverId: string;
275
+ serverName: string;
269
276
  health: ExternalMCPServerHealth;
270
277
  timestamp: Date;
271
278
  };
@@ -36,6 +36,11 @@ export declare class ExternalServerManager extends EventEmitter {
36
36
  * Get current HITL manager
37
37
  */
38
38
  getHITLManager(): HITLManager | undefined;
39
+ /**
40
+ * Resolve the human-readable server name for an event payload.
41
+ * Falls back to serverId if the instance or config.name isn't available.
42
+ */
43
+ getServerName(serverId: string): string;
39
44
  /**
40
45
  * Load MCP server configurations from .mcp-config.json file with parallel loading support
41
46
  * Automatically registers servers found in the configuration
@@ -194,10 +194,16 @@ export class ExternalServerManager extends EventEmitter {
194
194
  this.toolDiscovery = new ToolDiscoveryService();
195
195
  // Forward tool discovery events
196
196
  this.toolDiscovery.on("toolRegistered", (event) => {
197
- this.emit("toolDiscovered", event);
197
+ this.emit("toolDiscovered", {
198
+ ...event,
199
+ serverName: this.getServerName(event.serverId),
200
+ });
198
201
  });
199
202
  this.toolDiscovery.on("toolUnregistered", (event) => {
200
- this.emit("toolRemoved", event);
203
+ this.emit("toolRemoved", {
204
+ ...event,
205
+ serverName: this.getServerName(event.serverId),
206
+ });
201
207
  });
202
208
  // Handle process cleanup
203
209
  process.on("SIGINT", () => this.shutdown());
@@ -223,6 +229,14 @@ export class ExternalServerManager extends EventEmitter {
223
229
  getHITLManager() {
224
230
  return this.hitlManager;
225
231
  }
232
+ /**
233
+ * Resolve the human-readable server name for an event payload.
234
+ * Falls back to serverId if the instance or config.name isn't available.
235
+ */
236
+ getServerName(serverId) {
237
+ const instance = this.servers.get(serverId);
238
+ return instance?.config?.name || serverId;
239
+ }
226
240
  /**
227
241
  * Load MCP server configurations from .mcp-config.json file with parallel loading support
228
242
  * Automatically registers servers found in the configuration
@@ -712,6 +726,8 @@ export class ExternalServerManager extends EventEmitter {
712
726
  };
713
727
  }
714
728
  mcpLogger.info(`[ExternalServerManager] Removing server: ${serverId}`);
729
+ // Capture name before deletion removes the instance
730
+ const serverName = this.getServerName(serverId);
715
731
  // Stop the server
716
732
  await this.stopServer(serverId);
717
733
  // Remove from registry
@@ -719,6 +735,7 @@ export class ExternalServerManager extends EventEmitter {
719
735
  // Emit event
720
736
  this.emit("disconnected", {
721
737
  serverId,
738
+ serverName,
722
739
  reason: "Manually removed",
723
740
  timestamp: new Date(),
724
741
  });
@@ -816,6 +833,7 @@ export class ExternalServerManager extends EventEmitter {
816
833
  // Emit connected event
817
834
  this.emit("connected", {
818
835
  serverId,
836
+ serverName: this.getServerName(serverId),
819
837
  toolCount: instance.toolsMap.size,
820
838
  timestamp: new Date(),
821
839
  });
@@ -921,6 +939,7 @@ export class ExternalServerManager extends EventEmitter {
921
939
  // Emit status change event
922
940
  this.emit("statusChanged", {
923
941
  serverId,
942
+ serverName: this.getServerName(serverId),
924
943
  oldStatus,
925
944
  newStatus,
926
945
  timestamp: new Date(),
@@ -941,6 +960,7 @@ export class ExternalServerManager extends EventEmitter {
941
960
  // Emit failed event
942
961
  this.emit("failed", {
943
962
  serverId,
963
+ serverName: this.getServerName(serverId),
944
964
  error: error.message,
945
965
  timestamp: new Date(),
946
966
  });
@@ -965,6 +985,7 @@ export class ExternalServerManager extends EventEmitter {
965
985
  // Emit disconnected event
966
986
  this.emit("disconnected", {
967
987
  serverId,
988
+ serverName: this.getServerName(serverId),
968
989
  reason,
969
990
  timestamp: new Date(),
970
991
  });
@@ -1078,6 +1099,7 @@ export class ExternalServerManager extends EventEmitter {
1078
1099
  // Emit health check event
1079
1100
  this.emit("healthCheck", {
1080
1101
  serverId,
1102
+ serverName: this.getServerName(serverId),
1081
1103
  health,
1082
1104
  timestamp: new Date(),
1083
1105
  });
package/dist/neurolink.js CHANGED
@@ -50,7 +50,7 @@ import { createMemoryRetrievalTools } from "./memory/memoryRetrievalTools.js";
50
50
  import { getMetricsAggregator, MetricsAggregator, } from "./observability/metricsAggregator.js";
51
51
  import { SpanStatus, SpanType } from "./observability/types/spanTypes.js";
52
52
  import { SpanSerializer } from "./observability/utils/spanSerializer.js";
53
- import { flushOpenTelemetry, getLangfuseHealthStatus, initializeOpenTelemetry, isOpenTelemetryInitialized, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
53
+ import { flushOpenTelemetry, getLangfuseHealthStatus, initializeOpenTelemetry, isOpenTelemetryInitialized, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
54
54
  import { TaskManager } from "./tasks/taskManager.js";
55
55
  import { createTaskTools } from "./tasks/tools/taskTools.js";
56
56
  import { ATTR } from "./telemetry/attributes.js";
@@ -1129,7 +1129,10 @@ Current user's request: ${currentInput}`;
1129
1129
  * Supports additional users with per-user prompt and maxWords overrides.
1130
1130
  */
1131
1131
  storeMemoryInBackground(originalPrompt, responseContent, userId, additionalUsers) {
1132
- setImmediate(async () => {
1132
+ // Preserve AsyncLocalStorage context across setImmediate boundary so that
1133
+ // memory writes appear under the originating Langfuse trace instead of
1134
+ // becoming orphan spans.
1135
+ const wrappedMemoryWrite = runWithCurrentLangfuseContext(async () => {
1133
1136
  try {
1134
1137
  const client = this.ensureMemoryReady();
1135
1138
  if (!client) {
@@ -1145,12 +1148,18 @@ Current user's request: ${currentInput}`;
1145
1148
  : undefined;
1146
1149
  writeOps.push(client.add(user.userId, content, addOptions));
1147
1150
  }
1148
- await Promise.all(writeOps);
1151
+ // withTimeout races against Promise.all — if the timeout fires, the
1152
+ // await resolves with an error but the underlying client.add() calls
1153
+ // may still complete in the background. This is acceptable: the memory
1154
+ // client API (Mem0) doesn't support AbortSignal, and these are
1155
+ // fire-and-forget background writes where a stale completion is harmless.
1156
+ await withTimeout(Promise.all(writeOps), 30_000, new Error("Background memory write timed out after 30s"));
1149
1157
  }
1150
1158
  catch (error) {
1151
1159
  logger.warn("Memory storage failed:", error);
1152
1160
  }
1153
1161
  });
1162
+ setImmediate(wrappedMemoryWrite);
1154
1163
  }
1155
1164
  /**
1156
1165
  * Set up HITL event forwarding to main emitter
@@ -3723,6 +3732,21 @@ Current user's request: ${currentInput}`;
3723
3732
  conversationMessageCount: conversationMessages.length,
3724
3733
  shouldCompact: budgetResult.shouldCompact,
3725
3734
  });
3735
+ // Scale timeout for large contexts if caller didn't set one explicitly.
3736
+ // Providers read options.timeout via getTimeout(), so setting it here
3737
+ // propagates to any downstream provider call.
3738
+ if (options.timeout === undefined &&
3739
+ budgetResult.estimatedInputTokens > 100_000) {
3740
+ // >100K → 1.5x, >200K → 2x, >300K → 2.5x (capped at 4x) of 60s base
3741
+ const scale = 1 + Math.floor((budgetResult.estimatedInputTokens - 1) / 100_000) * 0.5;
3742
+ const scaledMs = Math.round(60_000 * Math.min(scale, 4));
3743
+ options.timeout = scaledMs;
3744
+ logger.info("[TokenBudget] Scaled timeout for large context", {
3745
+ requestId,
3746
+ estimatedTokens: budgetResult.estimatedInputTokens,
3747
+ scaledTimeoutMs: scaledMs,
3748
+ });
3749
+ }
3726
3750
  const compactionSessionId = this.getCompactionSessionId(options);
3727
3751
  const lastCompactionCount = this.lastCompactionMessageCount.get(compactionSessionId) ?? 0;
3728
3752
  if (!budgetResult.shouldCompact ||
@@ -3798,6 +3822,8 @@ Current user's request: ${currentInput}`;
3798
3822
  toolDefinitions: availableTools,
3799
3823
  });
3800
3824
  if (!finalBudget.withinBudget) {
3825
+ // Clear watermark so handleContextOverflow recovery can re-compact
3826
+ this.lastCompactionMessageCount.delete(compactionSessionId);
3801
3827
  throw new ContextBudgetExceededError(`Context exceeds model budget after all compaction stages. ` +
3802
3828
  `Estimated: ${finalBudget.estimatedInputTokens} tokens, ` +
3803
3829
  `Budget: ${finalBudget.availableInputTokens} tokens. ` +
@@ -3993,6 +4019,8 @@ Current user's request: ${currentInput}`;
3993
4019
  : undefined,
3994
4020
  });
3995
4021
  if (!finalBudget.withinBudget) {
4022
+ // Clear watermark so handleContextOverflow recovery can re-compact
4023
+ this.lastCompactionMessageCount.delete(dpgCompactionSessionId);
3996
4024
  throw new ContextBudgetExceededError(`Context exceeds model budget after all compaction stages. ` +
3997
4025
  `Estimated: ${finalBudget.estimatedInputTokens} tokens, ` +
3998
4026
  `Budget: ${finalBudget.availableInputTokens} tokens.`, {
@@ -5016,6 +5044,8 @@ Current user's request: ${currentInput}`;
5016
5044
  toolDefinitions: availableTools,
5017
5045
  });
5018
5046
  if (!finalBudget.withinBudget) {
5047
+ // Clear watermark so handleContextOverflow recovery can re-compact
5048
+ this.lastCompactionMessageCount.delete(streamCompactionSessionId);
5019
5049
  throw new ContextBudgetExceededError(`Stream context exceeds model budget after all compaction stages. ` +
5020
5050
  `Estimated: ${finalBudget.estimatedInputTokens} tokens, ` +
5021
5051
  `Budget: ${finalBudget.availableInputTokens} tokens.`, {
@@ -7508,6 +7538,7 @@ Current user's request: ${currentInput}`;
7508
7538
  // Emit server added event
7509
7539
  this.emitter.emit("externalMCP:serverAdded", {
7510
7540
  serverId,
7541
+ serverName: config.name || serverId,
7511
7542
  config,
7512
7543
  toolCount: result.metadata?.toolsDiscovered || 0,
7513
7544
  timestamp: Date.now(),
@@ -7535,12 +7566,15 @@ Current user's request: ${currentInput}`;
7535
7566
  this.invalidateToolCache(); // Invalidate cache when an external server is removed
7536
7567
  try {
7537
7568
  mcpLogger.info(`[NeuroLink] Removing external MCP server: ${serverId}`);
7569
+ // Capture the configured name before removal destroys the instance
7570
+ const serverName = this.externalServerManager.getServerName(serverId);
7538
7571
  const result = await this.externalServerManager.removeServer(serverId);
7539
7572
  if (result.success) {
7540
7573
  mcpLogger.info(`[NeuroLink] External MCP server removed successfully: ${serverId}`);
7541
7574
  // Emit server removed event
7542
7575
  this.emitter.emit("externalMCP:serverRemoved", {
7543
7576
  serverId,
7577
+ serverName,
7544
7578
  timestamp: Date.now(),
7545
7579
  });
7546
7580
  }
@@ -16,6 +16,12 @@ import type { AccountQuota } from "../types/index.js";
16
16
  * Pure computation — no I/O, no blocking.
17
17
  */
18
18
  export declare function parseQuotaHeaders(headers: Headers | Record<string, string>): AccountQuota | null;
19
+ /**
20
+ * Initialise the quota module with a custom file path.
21
+ * When set, all reads/writes go to this path instead of the default
22
+ * ~/.neurolink/account-quotas.json. Call before the first load/save.
23
+ */
24
+ export declare function initAccountQuota(quotaFilePath: string): void;
19
25
  /**
20
26
  * Load all persisted account quotas.
21
27
  * First call reads from disk; subsequent calls return the in-memory cache.
@@ -9,7 +9,7 @@
9
9
  * updates an in-memory cache and debounces disk writes so the request/response
10
10
  * path is never blocked by file I/O.
11
11
  */
12
- import { join } from "path";
12
+ import { dirname, join } from "path";
13
13
  import { homedir } from "os";
14
14
  import { promises as fs } from "fs";
15
15
  // ---------------------------------------------------------------------------
@@ -73,11 +73,32 @@ let memoryCache = {};
73
73
  let cacheLoaded = false;
74
74
  let dirty = false;
75
75
  let flushTimer = null;
76
+ /** Custom quota file path set via initAccountQuota(). */
77
+ let customQuotaFilePath = null;
78
+ /**
79
+ * Initialise the quota module with a custom file path.
80
+ * When set, all reads/writes go to this path instead of the default
81
+ * ~/.neurolink/account-quotas.json. Call before the first load/save.
82
+ */
83
+ export function initAccountQuota(quotaFilePath) {
84
+ customQuotaFilePath = quotaFilePath;
85
+ // Cancel any pending flush from a previous configuration so it does not
86
+ // write stale data to the new path.
87
+ if (flushTimer) {
88
+ clearTimeout(flushTimer);
89
+ flushTimer = null;
90
+ }
91
+ // Reset cache so the new path is picked up on next load
92
+ memoryCache = {};
93
+ cacheLoaded = false;
94
+ dirty = false;
95
+ }
76
96
  function getQuotaFilePath() {
77
- return join(homedir(), ".neurolink", QUOTA_FILE);
97
+ return customQuotaFilePath ?? join(homedir(), ".neurolink", QUOTA_FILE);
78
98
  }
79
99
  async function ensureDir() {
80
- const dir = join(homedir(), ".neurolink");
100
+ const filePath = getQuotaFilePath();
101
+ const dir = dirname(filePath);
81
102
  await fs.mkdir(dir, { recursive: true, mode: 0o700 }).catch(() => {
82
103
  // Non-fatal: directory may already exist
83
104
  });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Proxy file path resolver.
3
+ *
4
+ * In normal mode, all paths resolve under ~/.neurolink/.
5
+ * In dev mode (--dev), writable paths resolve under <cwd>/.neurolink-dev/
6
+ * so a local dev proxy never touches the global proxy's state.
7
+ *
8
+ * Read-only paths (like .env) always point to the global location
9
+ * since credentials must be shared.
10
+ *
11
+ * NOTE: Claude Code header snapshots (~/.neurolink/header-snapshots/) are
12
+ * not redirected in dev mode. They are only written when a real Claude Code
13
+ * client connects, which typically does not happen during dev testing.
14
+ */
15
+ export type ProxyPaths = {
16
+ /** Base directory for proxy state files */
17
+ stateDir: string;
18
+ /** logs/ — request/response logs */
19
+ logsDir: string;
20
+ /** account-quotas.json — per-account rate limit state */
21
+ quotaFile: string;
22
+ /** Whether this is a dev-mode isolated instance */
23
+ isDev: boolean;
24
+ };
25
+ export declare function resolveProxyPaths(dev: boolean): ProxyPaths;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Proxy file path resolver.
3
+ *
4
+ * In normal mode, all paths resolve under ~/.neurolink/.
5
+ * In dev mode (--dev), writable paths resolve under <cwd>/.neurolink-dev/
6
+ * so a local dev proxy never touches the global proxy's state.
7
+ *
8
+ * Read-only paths (like .env) always point to the global location
9
+ * since credentials must be shared.
10
+ *
11
+ * NOTE: Claude Code header snapshots (~/.neurolink/header-snapshots/) are
12
+ * not redirected in dev mode. They are only written when a real Claude Code
13
+ * client connects, which typically does not happen during dev testing.
14
+ */
15
+ import { homedir } from "node:os";
16
+ import { join } from "node:path";
17
+ export function resolveProxyPaths(dev) {
18
+ if (dev) {
19
+ const base = join(process.cwd(), ".neurolink-dev");
20
+ return {
21
+ stateDir: base,
22
+ logsDir: join(base, "logs"),
23
+ quotaFile: join(base, "account-quotas.json"),
24
+ isDev: true,
25
+ };
26
+ }
27
+ const base = join(homedir(), ".neurolink");
28
+ return {
29
+ stateDir: base,
30
+ logsDir: join(base, "logs"),
31
+ quotaFile: join(base, "account-quotas.json"),
32
+ isDev: false,
33
+ };
34
+ }
@@ -6,7 +6,7 @@
6
6
  * Useful for debugging and auditing proxy traffic.
7
7
  */
8
8
  import type { RequestAttemptLogEntry, RequestLogEntry } from "../types/index.js";
9
- export declare function initRequestLogger(enabled?: boolean): void;
9
+ export declare function initRequestLogger(enabled?: boolean, customLogsDir?: string): void;
10
10
  export declare function logRequest(entry: RequestLogEntry): Promise<void>;
11
11
  /**
12
12
  * Log an upstream attempt separately from the final request outcome.
@@ -44,13 +44,13 @@ const SENSITIVE_HEADER_NAMES = new Set([
44
44
  const SENSITIVE_HEADER_PATTERN = /token|secret|key|password|credential/i;
45
45
  /** JSON keys whose values should be redacted in request/response bodies. */
46
46
  const SENSITIVE_BODY_KEYS = /("(?:password|access_token|refresh_token|api_key|apiKey|secret|authorization|token|credential|x-api-key)"\s*:\s*)"(?:[^"\\]|\\.)*"/gi;
47
- export function initRequestLogger(enabled = true) {
47
+ export function initRequestLogger(enabled = true, customLogsDir) {
48
48
  logEnabled = enabled;
49
49
  if (!enabled) {
50
50
  return;
51
51
  }
52
52
  try {
53
- logDir = join(homedir(), ".neurolink", "logs");
53
+ logDir = customLogsDir ?? join(homedir(), ".neurolink", "logs");
54
54
  if (!existsSync(logDir)) {
55
55
  mkdirSync(logDir, { recursive: true, mode: 0o700 });
56
56
  }