@juspay/neurolink 12.34.0 → 12.34.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.
@@ -7,7 +7,6 @@
7
7
  * - Tool discovery and registration
8
8
  */
9
9
  import { EventEmitter } from "events";
10
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
11
10
  import { mcpLogger } from "../utils/logger.js";
12
11
  import { MCPClientFactory } from "./mcpClientFactory.js";
13
12
  import { ToolDiscoveryService } from "./toolDiscoveryService.js";
@@ -1063,6 +1062,7 @@ export class ExternalServerManager extends EventEmitter {
1063
1062
  instance.client = clientResult.client;
1064
1063
  instance.transportInstance = clientResult.transport;
1065
1064
  instance.process = clientResult.process || null;
1065
+ const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
1066
1066
  instance.pid =
1067
1067
  clientResult.transport instanceof StdioClientTransport
1068
1068
  ? (clientResult.transport.pid ?? undefined)
@@ -4,7 +4,7 @@
4
4
  * Supports stdio, SSE, WebSocket, and HTTP transports
5
5
  * Enhanced with retry, rate limiting, and OAuth 2.1 support
6
6
  */
7
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
+ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
8
8
  import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
9
9
  import type { ClientCapabilities } from "@modelcontextprotocol/sdk/types.js";
10
10
  import type { ChildProcess } from "child_process";
@@ -4,11 +4,6 @@
4
4
  * Supports stdio, SSE, WebSocket, and HTTP transports
5
5
  * Enhanced with retry, rate limiting, and OAuth 2.1 support
6
6
  */
7
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
9
- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
10
- import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js";
11
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
12
7
  import { mcpLogger } from "../utils/logger.js";
13
8
  import { globalCircuitBreakerManager } from "./mcpCircuitBreaker.js";
14
9
  import { CircuitBreakerOpenError } from "../types/index.js";
@@ -209,6 +204,7 @@ export class MCPClientFactory {
209
204
  const transport = transportResult.transport;
210
205
  try {
211
206
  // Create client
207
+ const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
212
208
  const client = new Client(this.NEUROLINK_IMPLEMENTATION, {
213
209
  capabilities: this.DEFAULT_CAPABILITIES,
214
210
  });
@@ -296,6 +292,7 @@ export class MCPClientFactory {
296
292
  if (!config.command) {
297
293
  throw new Error(`Command is required for stdio transport`);
298
294
  }
295
+ const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
299
296
  const transport = new StdioClientTransport({
300
297
  command: config.command,
301
298
  args: config.args || [],
@@ -333,6 +330,7 @@ export class MCPClientFactory {
333
330
  });
334
331
  try {
335
332
  const url = new URL(config.url);
333
+ const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js");
336
334
  const transport = new SSEClientTransport(url);
337
335
  return { transport };
338
336
  }
@@ -352,6 +350,7 @@ export class MCPClientFactory {
352
350
  });
353
351
  try {
354
352
  const url = new URL(config.url);
353
+ const { WebSocketClientTransport } = await import("@modelcontextprotocol/sdk/client/websocket.js");
355
354
  const transport = new WebSocketClientTransport(url);
356
355
  return { transport };
357
356
  }
@@ -402,6 +401,7 @@ export class MCPClientFactory {
402
401
  const requestInit = {
403
402
  headers: Object.keys(headers).length > 0 ? headers : undefined,
404
403
  };
404
+ const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
405
405
  const transport = new StreamableHTTPClientTransport(url, {
406
406
  requestInit,
407
407
  fetch: fetchWithEnhancements,
package/dist/neurolink.js CHANGED
@@ -2804,11 +2804,15 @@ Current user's request: ${currentInput}`;
2804
2804
  try {
2805
2805
  const langfuseConfig = this.observabilityConfig?.langfuse;
2806
2806
  if (langfuseConfig?.enabled) {
2807
- void initializeOpenTelemetry(langfuseConfig).catch((err) => {
2807
+ try {
2808
+ await initializeOpenTelemetry(langfuseConfig);
2809
+ }
2810
+ catch (err) {
2808
2811
  logger.error("[NeuroLink] OpenTelemetry initialization failed", {
2809
2812
  error: err instanceof Error ? err.message : String(err),
2810
2813
  });
2811
- });
2814
+ return;
2815
+ }
2812
2816
  logger.debug("[NeuroLink] Langfuse observability initialized via public method");
2813
2817
  }
2814
2818
  else {
@@ -12,6 +12,7 @@
12
12
  * `tool_use`, `tool_result`, `cache_control` and thinking blocks all survive
13
13
  * here because nothing reshapes them.
14
14
  */
15
+ import type { GoogleAuth } from "google-auth-library";
15
16
  import type { VertexAccessTokenProvider, UsageContext, VertexPassthroughTerminal, VertexAnthropicPassthroughRequest, VertexAnthropicPassthroughTarget } from "../types/index.js";
16
17
  /**
17
18
  * Translate `output_config` for Vertex.
@@ -40,6 +41,13 @@ export declare function buildVertexAnthropicPayload(body: Readonly<Record<string
40
41
  * caller's `stream` flag selects the verb rather than changing the body.
41
42
  */
42
43
  export declare function buildVertexAnthropicUrl(target: VertexAnthropicPassthroughTarget): string;
44
+ /** Exported only for continuous-test-suite-vertex-anthropic-fallback.ts,
45
+ * which proves concurrent callers share one instance. Not part of the
46
+ * package's public surface — this module isn't re-exported from index.ts. */
47
+ export declare function auth(): Promise<GoogleAuth>;
48
+ /** Test-only: isolates the concurrency proof from cache state any earlier
49
+ * test in the same process may have left behind. */
50
+ export declare function resetAuthCacheForTests(): void;
43
51
  /**
44
52
  * Replace the Google credential lookup, for tests only.
45
53
  *
@@ -12,7 +12,6 @@
12
12
  * `tool_use`, `tool_result`, `cache_control` and thinking blocks all survive
13
13
  * here because nothing reshapes them.
14
14
  */
15
- import { GoogleAuth } from "google-auth-library";
16
15
  import { logger } from "../utils/logger.js";
17
16
  import { isTransientNetworkError } from "./proxyFetch.js";
18
17
  /** Vertex pins the Anthropic API version in the body, not a header. */
@@ -379,16 +378,41 @@ export function buildVertexAnthropicUrl(target) {
379
378
  return (`https://${host}/v1/projects/${target.projectId}/locations/` +
380
379
  `${target.location}/publishers/anthropic/models/${target.model}:${verb}`);
381
380
  }
382
- let cachedAuth;
383
- function auth() {
384
- cachedAuth ??= new GoogleAuth({ scopes: [CLOUD_PLATFORM_SCOPE] });
385
- return cachedAuth;
381
+ // google-auth-library is only needed once a Vertex fallback request actually
382
+ // needs a Google access token, so it's loaded with `await import(...)` instead
383
+ // of a static import. The module promise is cached at module scope, and the
384
+ // resulting client instance is cached separately, exactly as before.
385
+ let googleAuthModulePromise;
386
+ function loadGoogleAuth() {
387
+ if (!googleAuthModulePromise) {
388
+ googleAuthModulePromise = import("google-auth-library");
389
+ }
390
+ return googleAuthModulePromise;
391
+ }
392
+ // Cache the PROMISE, not the resolved client: the guard below runs to
393
+ // completion before any await, so two calls entering before the first
394
+ // resolves both see the same in-flight promise instead of each racing to
395
+ // construct — and return — its own GoogleAuth instance.
396
+ let cachedAuthPromise;
397
+ /** Exported only for continuous-test-suite-vertex-anthropic-fallback.ts,
398
+ * which proves concurrent callers share one instance. Not part of the
399
+ * package's public surface — this module isn't re-exported from index.ts. */
400
+ export function auth() {
401
+ if (!cachedAuthPromise) {
402
+ cachedAuthPromise = loadGoogleAuth().then(({ GoogleAuth }) => new GoogleAuth({ scopes: [CLOUD_PLATFORM_SCOPE] }));
403
+ }
404
+ return cachedAuthPromise;
405
+ }
406
+ /** Test-only: isolates the concurrency proof from cache state any earlier
407
+ * test in the same process may have left behind. */
408
+ export function resetAuthCacheForTests() {
409
+ cachedAuthPromise = undefined;
386
410
  }
387
411
  let accessTokenProviderForTests;
388
412
  async function vertexAccessToken() {
389
413
  return accessTokenProviderForTests
390
414
  ? accessTokenProviderForTests()
391
- : auth().getAccessToken();
415
+ : (await auth()).getAccessToken();
392
416
  }
393
417
  /**
394
418
  * Replace the Google credential lookup, for tests only.
@@ -7,9 +7,9 @@
7
7
  * Flow: Vercel AI SDK → OpenTelemetry Spans → LangfuseSpanProcessor → Langfuse Platform
8
8
  */
9
9
  import { trace, type Span as ApiSpan } from "@opentelemetry/api";
10
- import { LoggerProvider } from "@opentelemetry/sdk-logs";
11
- import { type SpanProcessor } from "@opentelemetry/sdk-trace-base";
12
- import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
10
+ import type { LoggerProvider } from "@opentelemetry/sdk-logs";
11
+ import type { SpanProcessor } from "@opentelemetry/sdk-trace-base";
12
+ import type { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
13
13
  import type { LangfuseConfig, LangfuseContext } from "../../../../types/index.js";
14
14
  /**
15
15
  * True when a span is an internal NeuroLink wrapper that should NOT be sent to
@@ -38,18 +38,11 @@ export declare function langfuseShouldExportSpan({ otelSpan, }: {
38
38
  };
39
39
  }): boolean;
40
40
  /**
41
- * Initialize OpenTelemetry with Langfuse span processor
41
+ * Initialize OpenTelemetry once and let concurrent callers share the work.
42
42
  *
43
- * This connects Vercel AI SDK's experimental_telemetry to Langfuse by:
44
- * 1. Creating LangfuseSpanProcessor with Langfuse credentials
45
- * 2. Creating a NodeTracerProvider with service metadata and span processor
46
- * 3. Registering the provider globally for AI SDK to use
47
- *
48
- * NEW: If useExternalTracerProvider is true or autoDetectExternalProvider detects
49
- * an existing provider, steps 2 and 3 are skipped. The span processors are still
50
- * created and can be retrieved via getSpanProcessors().
51
- *
52
- * @param config - Langfuse configuration passed from parent application
43
+ * NeuroLink constructors intentionally start this asynchronously. Keeping the
44
+ * in-flight promise here lets another constructor join it and lets an immediate
45
+ * flush or shutdown wait for the same initialization before touching providers.
53
46
  */
54
47
  export declare function initializeOpenTelemetry(config: LangfuseConfig): Promise<void>;
55
48
  /**
@@ -7,23 +7,17 @@
7
7
  * Flow: Vercel AI SDK → OpenTelemetry Spans → LangfuseSpanProcessor → Langfuse Platform
8
8
  */
9
9
  import { metrics, SpanStatusCode, trace, } from "@opentelemetry/api";
10
- import { W3CTraceContextPropagator } from "@opentelemetry/core";
11
- import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
12
- import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
13
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
14
- import { resourceFromAttributes } from "@opentelemetry/resources";
15
- import { MeterProvider, PeriodicExportingMetricReader, } from "@opentelemetry/sdk-metrics";
16
- import { BatchLogRecordProcessor, LoggerProvider, } from "@opentelemetry/sdk-logs";
17
- import { BatchSpanProcessor, } from "@opentelemetry/sdk-trace-base";
18
- import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
19
- import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions";
20
10
  import { AsyncLocalStorage } from "async_hooks";
21
11
  import { extractMcpErrorText } from "../../../../utils/mcpErrorText.js";
22
12
  import { logger } from "../../../../utils/logger.js";
23
13
  import { tryImport } from "../../../../utils/tryImport.js";
24
14
  import { LANGFUSE_ATTR } from "../../../../telemetry/attributes.js";
25
15
  const LOG_PREFIX = "[OpenTelemetry]";
26
- function createOtelResource(config, serviceName) {
16
+ async function createOtelResource(config, serviceName) {
17
+ const [{ resourceFromAttributes }, { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION },] = await Promise.all([
18
+ import("@opentelemetry/resources"),
19
+ import("@opentelemetry/semantic-conventions"),
20
+ ]);
27
21
  return resourceFromAttributes({
28
22
  [ATTR_SERVICE_NAME]: serviceName,
29
23
  [ATTR_SERVICE_VERSION]: config.release || "v1.0.0",
@@ -32,13 +26,19 @@ function createOtelResource(config, serviceName) {
32
26
  }
33
27
  /**
34
28
  * Configure OTLP metrics and logs with bounded serialization batches for
35
- * request-serving processes.
29
+ * request-serving processes. Initialization awaits this so a caller may flush
30
+ * or shut down immediately after it returns without racing providers that have
31
+ * not been constructed yet. Signal-specific failures stay non-fatal here.
36
32
  */
37
- function initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName) {
33
+ async function initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName) {
38
34
  if (!otlpEndpoint) {
39
35
  return;
40
36
  }
41
37
  try {
38
+ const [{ OTLPMetricExporter }, { MeterProvider, PeriodicExportingMetricReader },] = await Promise.all([
39
+ import("@opentelemetry/exporter-metrics-otlp-http"),
40
+ import("@opentelemetry/sdk-metrics"),
41
+ ]);
42
42
  const metricExporter = new OTLPMetricExporter({
43
43
  url: `${otlpEndpoint}/v1/metrics`,
44
44
  });
@@ -68,6 +68,10 @@ function initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName) {
68
68
  });
69
69
  }
70
70
  try {
71
+ const [{ OTLPLogExporter }, { BatchLogRecordProcessor, LoggerProvider }] = await Promise.all([
72
+ import("@opentelemetry/exporter-logs-otlp-http"),
73
+ import("@opentelemetry/sdk-logs"),
74
+ ]);
71
75
  const logExporter = new OTLPLogExporter({
72
76
  url: `${otlpEndpoint}/v1/logs`,
73
77
  });
@@ -101,6 +105,7 @@ let meterProvider = null;
101
105
  let loggerProvider = null;
102
106
  let langfuseProcessor = null;
103
107
  let isInitialized = false;
108
+ let initializationPromise = null;
104
109
  let isCredentialsValid = false;
105
110
  let currentConfig = null;
106
111
  let usingExternalProvider = false;
@@ -637,8 +642,8 @@ async function initializeExternalOpenTelemetryMode(config, resource, otlpEndpoin
637
642
  ? await createLangfuseProcessor(config)
638
643
  : null;
639
644
  usingExternalProvider = true;
645
+ await initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName);
640
646
  isInitialized = true;
641
- initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName);
642
647
  try {
643
648
  const globalProvider = trace.getTracerProvider();
644
649
  const provider = globalProvider;
@@ -759,6 +764,10 @@ async function initializeStandaloneOpenTelemetryMode(config, resource, otlpEndpo
759
764
  }
760
765
  if (otlpEndpoint) {
761
766
  try {
767
+ const [{ OTLPTraceExporter }, { BatchSpanProcessor }] = await Promise.all([
768
+ import("@opentelemetry/exporter-trace-otlp-http"),
769
+ import("@opentelemetry/sdk-trace-base"),
770
+ ]);
762
771
  const otlpExporter = new OTLPTraceExporter({
763
772
  url: `${otlpEndpoint}/v1/traces`,
764
773
  });
@@ -782,13 +791,17 @@ async function initializeStandaloneOpenTelemetryMode(config, resource, otlpEndpo
782
791
  });
783
792
  }
784
793
  }
794
+ const [{ NodeTracerProvider }, { W3CTraceContextPropagator }] = await Promise.all([
795
+ import("@opentelemetry/sdk-trace-node"),
796
+ import("@opentelemetry/core"),
797
+ ]);
785
798
  tracerProvider = new NodeTracerProvider({ resource, spanProcessors });
786
799
  tracerProvider.register({
787
800
  propagator: new W3CTraceContextPropagator(),
788
801
  });
789
802
  usingExternalProvider = false;
803
+ await initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName);
790
804
  isInitialized = true;
791
- initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName);
792
805
  logger.info(`${LOG_PREFIX} Observability initialized`, {
793
806
  baseUrl: config.baseUrl || "https://cloud.langfuse.com",
794
807
  environment: config.environment || "dev",
@@ -820,6 +833,39 @@ async function initializeStandaloneOpenTelemetryMode(config, resource, otlpEndpo
820
833
  throw error;
821
834
  }
822
835
  }
836
+ /**
837
+ * Initialize OpenTelemetry once and let concurrent callers share the work.
838
+ *
839
+ * NeuroLink constructors intentionally start this asynchronously. Keeping the
840
+ * in-flight promise here lets another constructor join it and lets an immediate
841
+ * flush or shutdown wait for the same initialization before touching providers.
842
+ */
843
+ export async function initializeOpenTelemetry(config) {
844
+ if (isInitialized) {
845
+ currentConfig = config;
846
+ logger.debug(`${LOG_PREFIX} Already initialized, config updated`, {
847
+ usingExternalProvider,
848
+ hasLangfuseProcessor: !!langfuseProcessor,
849
+ hasTraceNameFormat: typeof config.traceNameFormat === "function",
850
+ });
851
+ return;
852
+ }
853
+ if (initializationPromise) {
854
+ await initializationPromise;
855
+ currentConfig = config;
856
+ return;
857
+ }
858
+ const pending = initializeOpenTelemetryOnce(config);
859
+ initializationPromise = pending;
860
+ try {
861
+ await pending;
862
+ }
863
+ finally {
864
+ if (initializationPromise === pending) {
865
+ initializationPromise = null;
866
+ }
867
+ }
868
+ }
823
869
  /**
824
870
  * Initialize OpenTelemetry with Langfuse span processor
825
871
  *
@@ -834,20 +880,7 @@ async function initializeStandaloneOpenTelemetryMode(config, resource, otlpEndpo
834
880
  *
835
881
  * @param config - Langfuse configuration passed from parent application
836
882
  */
837
- export async function initializeOpenTelemetry(config) {
838
- // Guard against multiple initializations — but always update config
839
- // so that later NeuroLink instances can change traceNameFormat,
840
- // autoDetectOperationName, and other configuration preferences
841
- // without re-initializing the OTEL infrastructure.
842
- if (isInitialized) {
843
- currentConfig = config;
844
- logger.debug(`${LOG_PREFIX} Already initialized, config updated`, {
845
- usingExternalProvider,
846
- hasLangfuseProcessor: !!langfuseProcessor,
847
- hasTraceNameFormat: typeof config.traceNameFormat === "function",
848
- });
849
- return;
850
- }
883
+ async function initializeOpenTelemetryOnce(config) {
851
884
  // FIRST: Check for external provider mode - bypasses enabled check
852
885
  // NOTE: When autoDetectExternalProvider is true, we trust the flag directly rather than
853
886
  // calling hasExternalTracerProvider(). This is because Neurolink may bundle its own copy
@@ -862,7 +895,7 @@ export async function initializeOpenTelemetry(config) {
862
895
  const langfuseRequested = config?.enabled === true;
863
896
  const hasLangfuseCreds = !!config.publicKey && !!config.secretKey;
864
897
  const serviceName = process.env.OTEL_SERVICE_NAME || "neurolink";
865
- const resource = createOtelResource(config, serviceName);
898
+ const resource = await createOtelResource(config, serviceName);
866
899
  if (shouldUseExternal) {
867
900
  await initializeExternalOpenTelemetryMode(config, resource, otlpEndpoint, serviceName, langfuseRequested, hasLangfuseCreds);
868
901
  return;
@@ -873,6 +906,15 @@ export async function initializeOpenTelemetry(config) {
873
906
  * Flush all pending spans to Langfuse
874
907
  */
875
908
  export async function flushOpenTelemetry() {
909
+ if (initializationPromise) {
910
+ try {
911
+ await initializationPromise;
912
+ }
913
+ catch {
914
+ // Initialization owns its error logging; a failed init leaves nothing
915
+ // initialized, so the check below skips the flush as it always has.
916
+ }
917
+ }
876
918
  if (!isInitialized) {
877
919
  logger.debug(`${LOG_PREFIX} Not initialized, skipping flush`);
878
920
  return;
@@ -951,8 +993,14 @@ export async function flushOpenTelemetry() {
951
993
  * Shutdown OpenTelemetry and Langfuse span processor
952
994
  */
953
995
  export async function shutdownOpenTelemetry() {
954
- if (!isInitialized) {
955
- return;
996
+ if (initializationPromise) {
997
+ try {
998
+ await initializationPromise;
999
+ }
1000
+ catch {
1001
+ // Initialization owns its error logging. Continue so any provider it
1002
+ // created before failing is still shut down below.
1003
+ }
956
1004
  }
957
1005
  try {
958
1006
  // Only shutdown tracerProvider if we created it
@@ -3,7 +3,6 @@
3
3
  * Helper functions for Redis storage operations
4
4
  */
5
5
  import { randomUUID } from "crypto";
6
- import { createClient } from "redis";
7
6
  import { logger } from "./logger.js";
8
7
  const SESSION_ONLY_PREFIX = "session-only:";
9
8
  /** Characters that end the authority (`user:pass@host:port`) of a URL in text. */
@@ -195,6 +194,9 @@ export async function createRedisClient(config) {
195
194
  if (config.password && !config.url) {
196
195
  clientOptions.password = config.password;
197
196
  }
197
+ // Imported here, not at the top: the redis client is ~590 modules, a quarter
198
+ // of everything importing the SDK loaded, and most callers never connect.
199
+ const { createClient } = await import("redis");
198
200
  // Create client with secured options
199
201
  const client = createClient(clientOptions);
200
202
  client.on("error", (err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.34.0",
3
+ "version": "12.34.1",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
6
6
  "author": {
@@ -274,6 +274,7 @@
274
274
  "test:aistudio-loop-characterization": "tsx test/continuous-test-suite-aistudio-loop-characterization.ts",
275
275
  "test:docs-mcp": "pnpm exec tsx test/continuous-test-suite-docs-mcp.ts",
276
276
  "test:docs-search-index": "pnpm exec tsx test/continuous-test-suite-docs-search-index.ts",
277
+ "test:import-cost": "pnpm exec tsx test/continuous-test-suite-import-cost.ts",
277
278
  "test:vertex-claude-characterization": "tsx test/continuous-test-suite-vertex-claude-characterization.ts",
278
279
  "test:toolfree-guard-overhead": "pnpm exec tsx test/continuous-test-suite-toolfree-guard-overhead.ts",
279
280
  "test:file-tool-roots": "pnpm exec tsx test/continuous-test-suite-file-tool-roots.ts",