@juspay/neurolink 12.48.1 → 12.49.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 (107) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/browser/neurolink.min.js +616 -595
  3. package/dist/cli/commands/serve.d.ts +5 -3
  4. package/dist/cli/commands/serve.js +113 -20
  5. package/dist/cli/commands/server.js +13 -6
  6. package/dist/cli/commands/transcribe.d.ts +24 -0
  7. package/dist/cli/commands/transcribe.js +718 -0
  8. package/dist/cli/factories/commandFactory.d.ts +10 -0
  9. package/dist/cli/factories/commandFactory.js +13 -0
  10. package/dist/cli/parser.js +3 -0
  11. package/dist/cli/utils/serverUtils.d.ts +28 -0
  12. package/dist/cli/utils/serverUtils.js +71 -0
  13. package/dist/factories/mediaHandlerCatalog.js +11 -9
  14. package/dist/factories/sttDescriptors.d.ts +63 -0
  15. package/dist/factories/sttDescriptors.js +286 -0
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.js +7 -1
  18. package/dist/neurolink.d.ts +68 -1
  19. package/dist/neurolink.js +202 -4
  20. package/dist/processors/media/AudioProcessor.d.ts +15 -56
  21. package/dist/processors/media/AudioProcessor.js +105 -276
  22. package/dist/processors/media/VideoProcessor.d.ts +3 -5
  23. package/dist/processors/media/VideoProcessor.js +52 -51
  24. package/dist/server/abstract/baseServerAdapter.d.ts +28 -1
  25. package/dist/server/abstract/baseServerAdapter.js +84 -4
  26. package/dist/server/adapters/expressAdapter.d.ts +1 -0
  27. package/dist/server/adapters/expressAdapter.js +50 -10
  28. package/dist/server/adapters/fastifyAdapter.d.ts +1 -0
  29. package/dist/server/adapters/fastifyAdapter.js +51 -8
  30. package/dist/server/adapters/honoAdapter.d.ts +1 -4
  31. package/dist/server/adapters/honoAdapter.js +95 -8
  32. package/dist/server/adapters/koaAdapter.d.ts +7 -0
  33. package/dist/server/adapters/koaAdapter.js +71 -10
  34. package/dist/server/index.d.ts +3 -2
  35. package/dist/server/index.js +3 -2
  36. package/dist/server/routes/index.d.ts +1 -0
  37. package/dist/server/routes/index.js +3 -0
  38. package/dist/server/routes/transcribeRoutes.d.ts +23 -0
  39. package/dist/server/routes/transcribeRoutes.js +506 -0
  40. package/dist/server/utils/rawBody.d.ts +32 -0
  41. package/dist/server/utils/rawBody.js +76 -0
  42. package/dist/server/utils/validation.d.ts +192 -0
  43. package/dist/server/utils/validation.js +100 -0
  44. package/dist/server/websocket/transcribeWebSocket.d.ts +42 -0
  45. package/dist/server/websocket/transcribeWebSocket.js +264 -0
  46. package/dist/types/cli.d.ts +47 -0
  47. package/dist/types/index.d.ts +1 -0
  48. package/dist/types/index.js +1 -0
  49. package/dist/types/providers.d.ts +7 -1
  50. package/dist/types/server.d.ts +117 -1
  51. package/dist/types/stt.d.ts +160 -7
  52. package/dist/types/transcribe.d.ts +506 -0
  53. package/dist/types/transcribe.js +29 -0
  54. package/dist/utils/speakerTurns.d.ts +28 -0
  55. package/dist/utils/speakerTurns.js +93 -0
  56. package/dist/utils/sttProcessor.d.ts +20 -2
  57. package/dist/utils/sttProcessor.js +73 -5
  58. package/dist/voice/correction/dictionary.d.ts +49 -0
  59. package/dist/voice/correction/dictionary.js +134 -0
  60. package/dist/voice/correction/guard.d.ts +35 -0
  61. package/dist/voice/correction/guard.js +161 -0
  62. package/dist/voice/correction/index.d.ts +47 -0
  63. package/dist/voice/correction/index.js +211 -0
  64. package/dist/voice/correction/rewrite.d.ts +92 -0
  65. package/dist/voice/correction/rewrite.js +322 -0
  66. package/dist/voice/errors.d.ts +15 -0
  67. package/dist/voice/errors.js +45 -0
  68. package/dist/voice/index.d.ts +20 -2
  69. package/dist/voice/index.js +75 -9
  70. package/dist/voice/providers/AzureSTT.d.ts +10 -7
  71. package/dist/voice/providers/AzureSTT.js +23 -75
  72. package/dist/voice/providers/DeepgramSTT.d.ts +17 -2
  73. package/dist/voice/providers/DeepgramSTT.js +109 -13
  74. package/dist/voice/providers/ElevenLabsSTT.d.ts +12 -2
  75. package/dist/voice/providers/ElevenLabsSTT.js +34 -8
  76. package/dist/voice/providers/GoogleSTT.d.ts +10 -8
  77. package/dist/voice/providers/GoogleSTT.js +21 -75
  78. package/dist/voice/providers/OpenAISTT.d.ts +30 -9
  79. package/dist/voice/providers/OpenAISTT.js +196 -21
  80. package/dist/voice/providers/WhistleSTT.d.ts +87 -0
  81. package/dist/voice/providers/WhistleSTT.js +313 -0
  82. package/dist/voice/streaming/chunkedStreamAdapter.d.ts +19 -0
  83. package/dist/voice/streaming/chunkedStreamAdapter.js +519 -0
  84. package/dist/voice/streaming/endpointer.d.ts +81 -0
  85. package/dist/voice/streaming/endpointer.js +247 -0
  86. package/dist/voice/streaming/localAgreement.d.ts +30 -0
  87. package/dist/voice/streaming/localAgreement.js +44 -0
  88. package/dist/voice/sttEnvCredentials.d.ts +12 -0
  89. package/dist/voice/sttEnvCredentials.js +12 -0
  90. package/dist/voice/transcribe.d.ts +47 -0
  91. package/dist/voice/transcribe.js +519 -0
  92. package/dist/voice/transcribeStream.d.ts +4 -0
  93. package/dist/voice/transcribeStream.js +584 -0
  94. package/dist/voice/whistle/assets.d.ts +47 -0
  95. package/dist/voice/whistle/assets.js +270 -0
  96. package/dist/voice/whistle/audio.d.ts +32 -0
  97. package/dist/voice/whistle/audio.js +267 -0
  98. package/dist/voice/whistle/engine.d.ts +32 -0
  99. package/dist/voice/whistle/engine.js +193 -0
  100. package/dist/voice/whistle/worker.d.ts +18 -0
  101. package/dist/voice/whistle/worker.js +294 -0
  102. package/docs-site/static/search-index.json +69 -10
  103. package/models/whistle/README.md +18 -0
  104. package/models/whistle/needle.js +2 -0
  105. package/models/whistle/needle.wasm +0 -0
  106. package/models/whistle/whistle.cact +0 -0
  107. package/package.json +7 -3
@@ -9,9 +9,6 @@
9
9
  * neurolink serve status
10
10
  */
11
11
  import type { CommandModule } from "yargs";
12
- /**
13
- * Serve CLI command factory
14
- */
15
12
  export declare class ServeCommandFactory {
16
13
  /**
17
14
  * Create the main serve command
@@ -36,6 +33,11 @@ export declare class ServeCommandFactory {
36
33
  * Returns a mutable reference wrapper so signal handlers always access the current server.
37
34
  */
38
35
  private static createAndStartServer;
36
+ /**
37
+ * Put every route except health behind the API key(s) in
38
+ * NEUROLINK_SERVER_API_KEY. Must run before the routes are registered.
39
+ */
40
+ private static applyApiKeyAuth;
39
41
  /**
40
42
  * Save server state and print the startup info banner.
41
43
  */
@@ -16,7 +16,7 @@ import { NeuroLink } from "../../neurolink.js";
16
16
  import { ConfigurationError, ServerStartError, } from "../../server/errors.js";
17
17
  import { withTimeout } from "../../utils/errorHandling.js";
18
18
  import { logger } from "../../utils/logger.js";
19
- import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
19
+ import { clearStateOnExit, createServerApiKeyMiddleware, formatUptime, isOtherServerRunning, isProcessRunning, readServerApiKeys, resolveServerPort, SERVER_API_KEY_ENV, StateFileManager, } from "../utils/serverUtils.js";
20
20
  // ============================================
21
21
  // State Management
22
22
  // ============================================
@@ -134,6 +134,50 @@ function createFileWatcher(onRestart, quiet) {
134
134
  /**
135
135
  * Serve CLI command factory
136
136
  */
137
+ /** A Node `http.Server` (or `https.Server`): it can take an `upgrade` listener. */
138
+ function isNodeHttpServer(value) {
139
+ return (typeof value === "object" &&
140
+ value !== null &&
141
+ typeof value.on === "function" &&
142
+ typeof value.listen === "function");
143
+ }
144
+ /**
145
+ * Live transcription streams over a WebSocket upgrade, which the HTTP
146
+ * adapters cannot route; it attaches to the Node server once it listens, on
147
+ * the first start and on every watch-mode restart. Bun and Deno serve
148
+ * without one, and then the route is simply absent. Guarded by the same
149
+ * NEUROLINK_SERVER_API_KEY(s) as the HTTP routes.
150
+ */
151
+ /** `--cors-origin` values, split on commas and trimmed; `undefined` when none were given. */
152
+ function corsOriginsFromArgv(values) {
153
+ if (!values || values.length === 0) {
154
+ return undefined;
155
+ }
156
+ const origins = values
157
+ .flatMap((value) => String(value).split(","))
158
+ .map((origin) => origin.trim())
159
+ .filter((origin) => origin.length > 0);
160
+ return origins.length > 0 ? origins : undefined;
161
+ }
162
+ async function attachTranscriptionWebSocket(server, neurolink, serverConfig) {
163
+ const native = server.getNativeServer?.();
164
+ if (!isNodeHttpServer(native)) {
165
+ return;
166
+ }
167
+ const { attachTranscribeWebSocket } = await import("../../server/websocket/transcribeWebSocket.js");
168
+ const apiKeys = readServerApiKeys();
169
+ // The same origin allow-list as the HTTP routes: with CORS on and a list
170
+ // that is not "*", a page from an unlisted origin cannot open the stream.
171
+ const origins = serverConfig.cors?.origins;
172
+ const allowedOrigins = serverConfig.cors?.enabled !== false && origins && !origins.includes("*")
173
+ ? origins
174
+ : undefined;
175
+ await attachTranscribeWebSocket(native, neurolink, {
176
+ ...(apiKeys.length > 0 ? { authToken: apiKeys } : {}),
177
+ ...(allowedOrigins ? { allowedOrigins } : {}),
178
+ });
179
+ logger.debug("[serve] transcription WebSocket attached at /v1/audio/transcriptions/stream");
180
+ }
137
181
  export class ServeCommandFactory {
138
182
  /**
139
183
  * Create the main serve command
@@ -143,6 +187,8 @@ export class ServeCommandFactory {
143
187
  command: "serve [subcommand]",
144
188
  describe: "Start NeuroLink HTTP server with server adapters",
145
189
  builder: (yargs) => {
190
+ // The server options carry no yargs defaults: a default would always
191
+ // win over the --config file. Defaults are applied after merging.
146
192
  return yargs
147
193
  .command("status", "Show server status", (yargs) => ServeCommandFactory.buildStatusOptions(yargs), (argv) => ServeCommandFactory.executeStatus(argv))
148
194
  .command("voice", "Start the real-time voice assistant server (OpenAI Realtime / Gemini Live)", (yargs) => yargs.option("port", {
@@ -157,43 +203,43 @@ export class ServeCommandFactory {
157
203
  .option("port", {
158
204
  type: "number",
159
205
  alias: "p",
160
- default: 3000,
161
- description: "Port to listen on",
206
+ description: "Port to listen on (default: config file, then $PORT, then 3000)",
162
207
  })
163
208
  .option("host", {
164
209
  type: "string",
165
210
  alias: "H",
166
- default: "0.0.0.0",
167
- description: "Host to bind to",
211
+ description: "Host to bind to (default: 0.0.0.0)",
168
212
  })
169
213
  .option("framework", {
170
214
  type: "string",
171
215
  alias: "f",
172
216
  choices: ["hono", "express", "fastify", "koa"],
173
- default: "hono",
174
- description: "Web framework to use (hono recommended)",
217
+ description: "Web framework to use (default: hono, recommended)",
175
218
  })
176
219
  .option("basePath", {
177
220
  type: "string",
178
221
  alias: "b",
179
- default: "/api",
180
- description: "Base path for all routes",
222
+ description: "Base path for all routes (default: /api)",
181
223
  })
182
224
  .option("cors", {
183
225
  type: "boolean",
184
- default: true,
185
- description: "Enable CORS middleware",
226
+ description: "Enable CORS middleware (default: true)",
227
+ })
228
+ .option("cors-origin", {
229
+ type: "array",
230
+ string: true,
231
+ alias: "corsOrigin",
232
+ description: "Browser origin allowed by CORS (repeat or comma-separate; '*' for any). " +
233
+ "Default: none, so no web page can call the server cross-origin",
186
234
  })
187
235
  .option("rate-limit", {
188
236
  type: "number",
189
237
  alias: "rateLimit",
190
- default: 100,
191
- description: "Rate limit (requests per 15 min window, 0 to disable)",
238
+ description: "Rate limit (requests per 15 min window, 0 to disable; default: 100)",
192
239
  })
193
240
  .option("swagger", {
194
241
  type: "boolean",
195
- default: false,
196
- description: "Enable OpenAPI/Swagger documentation",
242
+ description: "Enable OpenAPI/Swagger documentation (default: false)",
197
243
  })
198
244
  .option("config", {
199
245
  type: "string",
@@ -224,6 +270,7 @@ export class ServeCommandFactory {
224
270
  .example("neurolink serve --cors --rate-limit 50", "Start server with CORS and rate limiting (50 req/15min)")
225
271
  .example("neurolink serve --swagger", "Start server with OpenAPI documentation enabled")
226
272
  .example("neurolink serve --watch", "Start server in watch mode (restart on changes)")
273
+ .example("NEUROLINK_SERVER_API_KEY=secret neurolink serve", "Require 'Authorization: Bearer secret' (or X-API-Key) on every route except health")
227
274
  .example("neurolink serve status", "Show server status")
228
275
  .help();
229
276
  },
@@ -266,7 +313,7 @@ export class ServeCommandFactory {
266
313
  // Load config file if provided
267
314
  const fileConfig = ServeCommandFactory.loadFileConfig(argv, spinner);
268
315
  // Merge CLI args with file config (CLI takes precedence)
269
- const port = argv.port ?? fileConfig.port ?? 3000;
316
+ const port = resolveServerPort(argv.port, fileConfig.port);
270
317
  const host = argv.host ?? fileConfig.host ?? "0.0.0.0";
271
318
  const framework = argv.framework ?? fileConfig.framework ?? "hono";
272
319
  const basePath = argv.basePath ?? fileConfig.basePath ?? "/api";
@@ -307,7 +354,7 @@ export class ServeCommandFactory {
307
354
  */
308
355
  static guardAlreadyRunning(spinner) {
309
356
  const existingState = loadServeState();
310
- if (existingState && isProcessRunning(existingState.pid)) {
357
+ if (existingState && isOtherServerRunning(existingState.pid)) {
311
358
  if (spinner) {
312
359
  spinner.fail(chalk.red(`Server already running on port ${existingState.port} (PID: ${existingState.pid})`));
313
360
  }
@@ -355,6 +402,9 @@ export class ServeCommandFactory {
355
402
  enabled: argv.cors !== undefined
356
403
  ? argv.cors
357
404
  : (fileConfig.cors?.enabled ?? defaultConfig.cors.enabled),
405
+ ...(corsOriginsFromArgv(argv.corsOrigin)
406
+ ? { origins: corsOriginsFromArgv(argv.corsOrigin) }
407
+ : {}),
358
408
  },
359
409
  rateLimit: {
360
410
  ...defaultConfig.rateLimit,
@@ -370,7 +420,13 @@ export class ServeCommandFactory {
370
420
  },
371
421
  bodyParser: fileConfig.bodyParser,
372
422
  logging: fileConfig.logging,
373
- timeout: fileConfig.timeout,
423
+ // Transcription of a long recording through a remote engine can take
424
+ // longer than the 30 s default, so the request timeout is also
425
+ // settable from the environment (milliseconds); the config file wins.
426
+ timeout: fileConfig.timeout ??
427
+ (Number(process.env.NEUROLINK_SERVER_TIMEOUT_MS) > 0
428
+ ? Number(process.env.NEUROLINK_SERVER_TIMEOUT_MS)
429
+ : undefined),
374
430
  enableMetrics: fileConfig.enableMetrics ?? true,
375
431
  enableSwagger: argv.swagger !== undefined
376
432
  ? argv.swagger
@@ -391,14 +447,26 @@ export class ServeCommandFactory {
391
447
  config: opts.serverConfig,
392
448
  }),
393
449
  };
450
+ ServeCommandFactory.applyApiKeyAuth(serverRef.current, opts.basePath);
394
451
  registerAllRoutes(serverRef.current, opts.basePath);
395
452
  if (spinner) {
396
453
  spinner.text = "Initializing server...";
397
454
  }
398
455
  await withTimeout(serverRef.current.initialize(), 30000, new ServerStartError("Server initialization timed out after 30 seconds", undefined, opts.port, opts.host));
399
456
  await withTimeout(serverRef.current.start(), 30000, new ServerStartError("Server startup timed out after 30 seconds", undefined, opts.port, opts.host));
457
+ await attachTranscriptionWebSocket(serverRef.current, neurolink, opts.serverConfig);
400
458
  return serverRef;
401
459
  }
460
+ /**
461
+ * Put every route except health behind the API key(s) in
462
+ * NEUROLINK_SERVER_API_KEY. Must run before the routes are registered.
463
+ */
464
+ static applyApiKeyAuth(server, basePath) {
465
+ const apiKeys = readServerApiKeys();
466
+ if (apiKeys.length > 0) {
467
+ server.registerMiddleware(createServerApiKeyMiddleware(apiKeys, basePath));
468
+ }
469
+ }
402
470
  /**
403
471
  * Save server state and print the startup info banner.
404
472
  */
@@ -413,11 +481,17 @@ export class ServeCommandFactory {
413
481
  configFile: opts.argv.config,
414
482
  };
415
483
  saveServeState(state);
484
+ clearStateOnExit(clearServeState);
416
485
  if (spinner) {
417
486
  spinner.succeed(chalk.green("NeuroLink server started successfully"));
418
487
  }
419
488
  const url = `http://${opts.host === "0.0.0.0" ? "localhost" : opts.host}:${opts.port}`;
420
489
  const corsEnabled = opts.argv.cors ?? opts.serverConfig.cors?.enabled ?? true;
490
+ // The origin list and the API key together decide whether any browser
491
+ // page may call this server; the banner says which case this is.
492
+ const corsOrigins = opts.serverConfig.cors?.origins ?? [];
493
+ const corsWildcard = corsEnabled && corsOrigins.includes("*");
494
+ const corsNoOrigins = corsEnabled && corsOrigins.length === 0;
421
495
  const rateLimitValue = opts.argv.rateLimit ?? opts.serverConfig.rateLimit?.maxRequests ?? 100;
422
496
  const rateLimitEnabled = rateLimitValue > 0;
423
497
  const swaggerEnabled = opts.argv.swagger ?? opts.serverConfig.enableSwagger ?? false;
@@ -428,10 +502,13 @@ export class ServeCommandFactory {
428
502
  pid: state.pid,
429
503
  configFile: opts.argv.config,
430
504
  corsEnabled,
505
+ corsWildcard,
506
+ corsNoOrigins,
431
507
  rateLimitEnabled,
432
508
  rateLimitValue,
433
509
  swaggerEnabled,
434
510
  watchEnabled: opts.argv.watch ?? false,
511
+ authEnabled: readServerApiKeys().length > 0,
435
512
  });
436
513
  return state;
437
514
  }
@@ -452,12 +529,22 @@ export class ServeCommandFactory {
452
529
  }
453
530
  logger.always("");
454
531
  logger.always(chalk.bold("Middleware:"));
455
- logger.always(` CORS: ${info.corsEnabled ? chalk.green("enabled") : chalk.yellow("disabled")}`);
532
+ logger.always(` CORS: ${!info.corsEnabled
533
+ ? chalk.yellow("disabled")
534
+ : info.corsWildcard
535
+ ? info.authEnabled
536
+ ? chalk.green("enabled (any origin; API key required)")
537
+ : chalk.yellow("enabled for ANY origin and no API key — any web page can call this server; set " +
538
+ SERVER_API_KEY_ENV)
539
+ : info.corsNoOrigins
540
+ ? chalk.yellow("enabled, no origins listed — browsers cannot call this server; pass --cors-origin or set cors.origins")
541
+ : chalk.green("enabled (listed origins)")}`);
456
542
  logger.always(` Rate Limit: ${info.rateLimitEnabled ? chalk.green(`enabled (${info.rateLimitValue} req/15min)`) : chalk.yellow("disabled")}`);
457
543
  logger.always(` Swagger: ${info.swaggerEnabled ? chalk.green("enabled") : chalk.yellow("disabled")}`);
458
544
  if (info.watchEnabled) {
459
545
  logger.always(` Watch Mode: ${chalk.green("enabled")}`);
460
546
  }
547
+ logger.always(` API Key: ${info.authEnabled ? chalk.green("required (health routes exempt)") : chalk.yellow(`disabled — set ${SERVER_API_KEY_ENV} before exposing this server`)}`);
461
548
  logger.always("");
462
549
  logger.always(chalk.bold("Available Endpoints:"));
463
550
  logger.always(chalk.gray(" Health & Monitoring:"));
@@ -503,14 +590,20 @@ export class ServeCommandFactory {
503
590
  const timestamp = Date.now();
504
591
  const { createServer: createNewServer, registerAllRoutes: registerNewRoutes, } = await import(`../../server/index.js?t=${timestamp}`);
505
592
  // Create new server
506
- const newServer = await createNewServer(new NeuroLink(), {
593
+ const newNeurolink = new NeuroLink();
594
+ const newServer = await createNewServer(newNeurolink, {
507
595
  framework: ctx.framework,
508
596
  config: ctx.serverConfig,
509
597
  });
598
+ ServeCommandFactory.applyApiKeyAuth(newServer, ctx.basePath);
510
599
  registerNewRoutes(newServer, ctx.basePath);
511
600
  // Initialize and start with timeouts
512
601
  await withTimeout(newServer.initialize(), 30000, new ServerStartError("Server initialization timed out during restart", undefined, ctx.port, ctx.host));
513
602
  await withTimeout(newServer.start(), 30000, new ServerStartError("Server startup timed out during restart", undefined, ctx.port, ctx.host));
603
+ // The upgrade handler lives on the old adapter's http.Server; the
604
+ // restarted one needs its own, or the stream route is gone after the
605
+ // first file change.
606
+ await attachTranscriptionWebSocket(newServer, newNeurolink, ctx.serverConfig);
514
607
  // Update the reference so signal handlers use the new server instance
515
608
  ctx.serverRef.current = newServer;
516
609
  logger.always(chalk.green("Server restarted successfully"));
@@ -9,7 +9,7 @@ import path from "path";
9
9
  import { logger } from "../../utils/logger.js";
10
10
  import { NeuroLink } from "../../neurolink.js";
11
11
  import { withTimeout } from "../../utils/errorHandling.js";
12
- import { isProcessRunning, formatUptime, ensureStateDir, getNeuroLinkDir, } from "../utils/serverUtils.js";
12
+ import { isProcessRunning, formatUptime, ensureStateDir, getNeuroLinkDir, clearStateOnExit, createServerApiKeyMiddleware, isOtherServerRunning, readServerApiKeys, resolveServerPort, SERVER_API_KEY_ENV, } from "../utils/serverUtils.js";
13
13
  // ============================================
14
14
  // State Management
15
15
  // ============================================
@@ -160,8 +160,7 @@ export class ServerCommandFactory {
160
160
  .option("port", {
161
161
  type: "number",
162
162
  alias: "p",
163
- default: 3000,
164
- description: "Port to listen on",
163
+ description: "Port to listen on (default: $PORT, then 3000)",
165
164
  })
166
165
  .option("host", {
167
166
  type: "string",
@@ -344,7 +343,7 @@ export class ServerCommandFactory {
344
343
  try {
345
344
  // Check if server is already running
346
345
  const existingState = loadServerState();
347
- if (existingState && isProcessRunning(existingState.pid)) {
346
+ if (existingState && isOtherServerRunning(existingState.pid)) {
348
347
  if (spinner) {
349
348
  spinner.fail(chalk.red(`Server already running on port ${existingState.port} (PID: ${existingState.pid})`));
350
349
  }
@@ -355,12 +354,13 @@ export class ServerCommandFactory {
355
354
  const neurolink = new NeuroLink();
356
355
  // Dynamically import server module
357
356
  const { createServer, registerAllRoutes } = await import("../../server/index.js");
357
+ const port = resolveServerPort(argv.port);
358
358
  const framework = (argv.framework ?? "hono");
359
359
  // Create server
360
360
  const server = await createServer(neurolink, {
361
361
  framework,
362
362
  config: {
363
- port: argv.port ?? 3000,
363
+ port,
364
364
  host: argv.host ?? "0.0.0.0",
365
365
  basePath: argv.basePath ?? "/api",
366
366
  cors: {
@@ -372,6 +372,11 @@ export class ServerCommandFactory {
372
372
  disableBuiltInHealth: true, // We register health routes separately
373
373
  },
374
374
  });
375
+ // API key (if configured) must be registered ahead of the routes it guards
376
+ const apiKeys = readServerApiKeys();
377
+ if (apiKeys.length > 0) {
378
+ server.registerMiddleware(createServerApiKeyMiddleware(apiKeys, argv.basePath ?? "/api"));
379
+ }
375
380
  // Register all routes
376
381
  registerAllRoutes(server, argv.basePath ?? "/api");
377
382
  // Initialize and start with timeout
@@ -380,13 +385,14 @@ export class ServerCommandFactory {
380
385
  // Save state
381
386
  const state = {
382
387
  pid: process.pid,
383
- port: argv.port ?? 3000,
388
+ port,
384
389
  host: argv.host ?? "0.0.0.0",
385
390
  framework,
386
391
  startTime: new Date().toISOString(),
387
392
  basePath: argv.basePath ?? "/api",
388
393
  };
389
394
  saveServerState(state);
395
+ clearStateOnExit(clearServerState);
390
396
  if (spinner) {
391
397
  spinner.succeed(chalk.green("Server started successfully"));
392
398
  }
@@ -396,6 +402,7 @@ export class ServerCommandFactory {
396
402
  logger.always(` Framework: ${chalk.cyan(framework)}`);
397
403
  logger.always(` Base Path: ${chalk.cyan(state.basePath)}`);
398
404
  logger.always(` PID: ${chalk.cyan(state.pid)}`);
405
+ logger.always(` API Key: ${apiKeys.length > 0 ? chalk.green("required (health routes exempt)") : chalk.yellow(`disabled — set ${SERVER_API_KEY_ENV} before exposing this server`)}`);
399
406
  logger.always(chalk.bold("\nAvailable Endpoints:"));
400
407
  logger.always(` ${chalk.green("GET")} ${state.basePath}/health`);
401
408
  logger.always(` ${chalk.green("GET")} ${state.basePath}/ready`);
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * NeuroLink CLI Transcribe Command
4
+ *
5
+ * Calls `NeuroLink.transcribe()` — audio in, text out — or, with `--stream`,
6
+ * `NeuroLink.transcribeStream()` over the file decoded to 16 kHz mono PCM16.
7
+ * See src/lib/types/transcribe.ts for the full vocabulary (dictionary,
8
+ * correction, second opinion, fallback).
9
+ *
10
+ * Credentials are env-only here, exactly as every other CLI command: each STT
11
+ * handler reads its own settings, and `NEUROLINK_STT_PROVIDER`,
12
+ * `NEUROLINK_STT_MODEL` and `NEUROLINK_STT_LANGUAGE` pick the defaults. With
13
+ * nothing configured the built-in local engine (Whistle) is used.
14
+ */
15
+ import type { CommandModule } from "yargs";
16
+ import type { CliTranscribeArgs } from "../../types/index.js";
17
+ export declare const transcribeCommand: CommandModule<object, CliTranscribeArgs>;
18
+ /**
19
+ * Create transcribe command factory for CLICommandFactory-style registration,
20
+ * matching the DecideCommandFactory pattern.
21
+ */
22
+ export declare class TranscribeCommandFactory {
23
+ static createTranscribeCommand(): CommandModule;
24
+ }