@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.
- package/CHANGELOG.md +3 -3
- package/dist/browser/neurolink.min.js +616 -595
- package/dist/cli/commands/serve.d.ts +5 -3
- package/dist/cli/commands/serve.js +113 -20
- package/dist/cli/commands/server.js +13 -6
- package/dist/cli/commands/transcribe.d.ts +24 -0
- package/dist/cli/commands/transcribe.js +718 -0
- package/dist/cli/factories/commandFactory.d.ts +10 -0
- package/dist/cli/factories/commandFactory.js +13 -0
- package/dist/cli/parser.js +3 -0
- package/dist/cli/utils/serverUtils.d.ts +28 -0
- package/dist/cli/utils/serverUtils.js +71 -0
- package/dist/factories/mediaHandlerCatalog.js +11 -9
- package/dist/factories/sttDescriptors.d.ts +63 -0
- package/dist/factories/sttDescriptors.js +286 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +7 -1
- package/dist/neurolink.d.ts +68 -1
- package/dist/neurolink.js +202 -4
- package/dist/processors/media/AudioProcessor.d.ts +15 -56
- package/dist/processors/media/AudioProcessor.js +105 -276
- package/dist/processors/media/VideoProcessor.d.ts +3 -5
- package/dist/processors/media/VideoProcessor.js +52 -51
- package/dist/server/abstract/baseServerAdapter.d.ts +28 -1
- package/dist/server/abstract/baseServerAdapter.js +84 -4
- package/dist/server/adapters/expressAdapter.d.ts +1 -0
- package/dist/server/adapters/expressAdapter.js +50 -10
- package/dist/server/adapters/fastifyAdapter.d.ts +1 -0
- package/dist/server/adapters/fastifyAdapter.js +51 -8
- package/dist/server/adapters/honoAdapter.d.ts +1 -4
- package/dist/server/adapters/honoAdapter.js +95 -8
- package/dist/server/adapters/koaAdapter.d.ts +7 -0
- package/dist/server/adapters/koaAdapter.js +71 -10
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.js +3 -2
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.js +3 -0
- package/dist/server/routes/transcribeRoutes.d.ts +23 -0
- package/dist/server/routes/transcribeRoutes.js +506 -0
- package/dist/server/utils/rawBody.d.ts +32 -0
- package/dist/server/utils/rawBody.js +76 -0
- package/dist/server/utils/validation.d.ts +192 -0
- package/dist/server/utils/validation.js +100 -0
- package/dist/server/websocket/transcribeWebSocket.d.ts +42 -0
- package/dist/server/websocket/transcribeWebSocket.js +264 -0
- package/dist/types/cli.d.ts +47 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/providers.d.ts +7 -1
- package/dist/types/server.d.ts +117 -1
- package/dist/types/stt.d.ts +160 -7
- package/dist/types/transcribe.d.ts +506 -0
- package/dist/types/transcribe.js +29 -0
- package/dist/utils/speakerTurns.d.ts +28 -0
- package/dist/utils/speakerTurns.js +93 -0
- package/dist/utils/sttProcessor.d.ts +20 -2
- package/dist/utils/sttProcessor.js +73 -5
- package/dist/voice/correction/dictionary.d.ts +49 -0
- package/dist/voice/correction/dictionary.js +134 -0
- package/dist/voice/correction/guard.d.ts +35 -0
- package/dist/voice/correction/guard.js +161 -0
- package/dist/voice/correction/index.d.ts +47 -0
- package/dist/voice/correction/index.js +211 -0
- package/dist/voice/correction/rewrite.d.ts +92 -0
- package/dist/voice/correction/rewrite.js +322 -0
- package/dist/voice/errors.d.ts +15 -0
- package/dist/voice/errors.js +45 -0
- package/dist/voice/index.d.ts +20 -2
- package/dist/voice/index.js +75 -9
- package/dist/voice/providers/AzureSTT.d.ts +10 -7
- package/dist/voice/providers/AzureSTT.js +23 -75
- package/dist/voice/providers/DeepgramSTT.d.ts +17 -2
- package/dist/voice/providers/DeepgramSTT.js +109 -13
- package/dist/voice/providers/ElevenLabsSTT.d.ts +12 -2
- package/dist/voice/providers/ElevenLabsSTT.js +34 -8
- package/dist/voice/providers/GoogleSTT.d.ts +10 -8
- package/dist/voice/providers/GoogleSTT.js +21 -75
- package/dist/voice/providers/OpenAISTT.d.ts +30 -9
- package/dist/voice/providers/OpenAISTT.js +196 -21
- package/dist/voice/providers/WhistleSTT.d.ts +87 -0
- package/dist/voice/providers/WhistleSTT.js +313 -0
- package/dist/voice/streaming/chunkedStreamAdapter.d.ts +19 -0
- package/dist/voice/streaming/chunkedStreamAdapter.js +519 -0
- package/dist/voice/streaming/endpointer.d.ts +81 -0
- package/dist/voice/streaming/endpointer.js +247 -0
- package/dist/voice/streaming/localAgreement.d.ts +30 -0
- package/dist/voice/streaming/localAgreement.js +44 -0
- package/dist/voice/sttEnvCredentials.d.ts +12 -0
- package/dist/voice/sttEnvCredentials.js +12 -0
- package/dist/voice/transcribe.d.ts +47 -0
- package/dist/voice/transcribe.js +519 -0
- package/dist/voice/transcribeStream.d.ts +4 -0
- package/dist/voice/transcribeStream.js +584 -0
- package/dist/voice/whistle/assets.d.ts +47 -0
- package/dist/voice/whistle/assets.js +270 -0
- package/dist/voice/whistle/audio.d.ts +32 -0
- package/dist/voice/whistle/audio.js +267 -0
- package/dist/voice/whistle/engine.d.ts +32 -0
- package/dist/voice/whistle/engine.js +193 -0
- package/dist/voice/whistle/worker.d.ts +18 -0
- package/dist/voice/whistle/worker.js +294 -0
- package/docs-site/static/search-index.json +69 -10
- package/models/whistle/README.md +18 -0
- package/models/whistle/needle.js +2 -0
- package/models/whistle/needle.wasm +0 -0
- package/models/whistle/whistle.cact +0 -0
- 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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
+
}
|