@juspay/neurolink 9.95.3 → 10.0.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.
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console, curly */
3
2
  /**
4
3
  * NeuroLink CLI Observability Commands
5
4
  *
@@ -14,6 +13,7 @@ import ora from "ora";
14
13
  import { logger } from "../../lib/utils/logger.js";
15
14
  import { NeuroLink } from "../../lib/neurolink.js";
16
15
  import { formatRow, formatCost } from "../utils/formatters.js";
16
+ import { redactUrlCredentials } from "../../lib/utils/logSanitize.js";
17
17
  /**
18
18
  * Observability Command Factory
19
19
  */
@@ -70,67 +70,72 @@ export class ObservabilityCommandFactory {
70
70
  try {
71
71
  const neurolink = new NeuroLink();
72
72
  const status = neurolink.getTelemetryStatus();
73
- if (spinner)
73
+ if (spinner) {
74
74
  spinner.succeed("Status retrieved");
75
+ }
75
76
  if (args.format === "json") {
76
- console.log(JSON.stringify(status, null, 2));
77
+ logger.always(JSON.stringify(status, (_key, value) => typeof value === "string" &&
78
+ (_key === "baseUrl" || _key === "endpoint")
79
+ ? redactUrlCredentials(value)
80
+ : value, 2));
77
81
  }
78
82
  else {
79
- console.log("");
80
- console.log(chalk.bold.cyan("=== Observability Status ==="));
81
- console.log("");
83
+ logger.always("");
84
+ logger.always(chalk.bold.cyan("=== Observability Status ==="));
85
+ logger.always("");
82
86
  // Telemetry enabled status
83
87
  const enabledIcon = status.enabled
84
88
  ? chalk.green("ON")
85
89
  : chalk.red("OFF");
86
- console.log(formatRow("Telemetry:", enabledIcon));
90
+ logger.always(formatRow("Telemetry:", enabledIcon));
87
91
  // Langfuse status
88
92
  if (status.langfuse) {
89
- console.log("");
90
- console.log(chalk.bold("Langfuse:"));
93
+ logger.always("");
94
+ logger.always(chalk.bold("Langfuse:"));
91
95
  const lfStatus = status.langfuse.enabled
92
96
  ? chalk.green("Enabled")
93
97
  : chalk.gray("Disabled");
94
- console.log(formatRow(" Status:", lfStatus));
98
+ logger.always(formatRow(" Status:", lfStatus));
95
99
  if (status.langfuse.baseUrl) {
96
- console.log(formatRow(" URL:", status.langfuse.baseUrl));
100
+ logger.always(formatRow(" URL:", redactUrlCredentials(status.langfuse.baseUrl)));
97
101
  }
98
102
  if (status.langfuse.environment) {
99
- console.log(formatRow(" Environment:", status.langfuse.environment));
103
+ logger.always(formatRow(" Environment:", status.langfuse.environment));
100
104
  }
101
105
  }
102
106
  // OpenTelemetry status
103
107
  if (status.openTelemetry) {
104
- console.log("");
105
- console.log(chalk.bold("OpenTelemetry:"));
108
+ logger.always("");
109
+ logger.always(chalk.bold("OpenTelemetry:"));
106
110
  const otelStatus = status.openTelemetry.enabled
107
111
  ? chalk.green("Enabled")
108
112
  : chalk.gray("Disabled");
109
- console.log(formatRow(" Status:", otelStatus));
113
+ logger.always(formatRow(" Status:", otelStatus));
110
114
  if (status.openTelemetry.endpoint) {
111
- console.log(formatRow(" Endpoint:", status.openTelemetry.endpoint));
115
+ logger.always(formatRow(" Endpoint:", redactUrlCredentials(status.openTelemetry.endpoint)));
112
116
  }
113
117
  if (status.openTelemetry.serviceName) {
114
- console.log(formatRow(" Service:", status.openTelemetry.serviceName));
118
+ logger.always(formatRow(" Service:", status.openTelemetry.serviceName));
115
119
  }
116
120
  }
117
121
  // Exporters summary
118
122
  if (status.exporters && status.exporters.length > 0) {
119
- console.log("");
120
- console.log(chalk.bold("Active Exporters:"));
123
+ logger.always("");
124
+ logger.always(chalk.bold("Active Exporters:"));
121
125
  for (const exporter of status.exporters) {
122
126
  const exporterStatus = exporter.healthy
123
127
  ? chalk.green("Healthy")
124
128
  : chalk.red("Unhealthy");
125
- console.log(formatRow(` ${exporter.name}:`, exporterStatus));
129
+ logger.always(formatRow(` ${exporter.name}:`, exporterStatus));
126
130
  }
127
131
  }
128
- console.log("");
132
+ logger.always("");
129
133
  }
130
134
  }
131
135
  catch (error) {
132
- if (spinner)
136
+ if (spinner) {
133
137
  spinner.fail("Failed to get status");
138
+ }
134
139
  logger.error("Error:", error instanceof Error ? error.message : String(error));
135
140
  process.exit(1);
136
141
  }
@@ -171,74 +176,76 @@ export class ObservabilityCommandFactory {
171
176
  try {
172
177
  const neurolink = new NeuroLink();
173
178
  const metrics = neurolink.getMetrics();
174
- if (spinner)
179
+ if (spinner) {
175
180
  spinner.succeed("Metrics retrieved");
181
+ }
176
182
  if (args.format === "json") {
177
- console.log(JSON.stringify(metrics, null, 2));
183
+ logger.always(JSON.stringify(metrics, null, 2));
178
184
  }
179
185
  else {
180
- console.log("");
181
- console.log(chalk.bold.cyan("=== Metrics Summary ==="));
182
- console.log("");
186
+ logger.always("");
187
+ logger.always(chalk.bold.cyan("=== Metrics Summary ==="));
188
+ logger.always("");
183
189
  // Request statistics
184
- console.log(chalk.bold("Request Statistics:"));
185
- console.log(formatRow(" Total requests:", metrics.totalSpans.toLocaleString()));
186
- console.log(formatRow(" Successful:", metrics.successfulSpans.toLocaleString()));
187
- console.log(formatRow(" Failed:", metrics.failedSpans.toLocaleString()));
188
- console.log(formatRow(" Success rate:", `${(metrics.successRate * 100).toFixed(2)}%`));
190
+ logger.always(chalk.bold("Request Statistics:"));
191
+ logger.always(formatRow(" Total requests:", metrics.totalSpans.toLocaleString()));
192
+ logger.always(formatRow(" Successful:", metrics.successfulSpans.toLocaleString()));
193
+ logger.always(formatRow(" Failed:", metrics.failedSpans.toLocaleString()));
194
+ logger.always(formatRow(" Success rate:", `${(metrics.successRate * 100).toFixed(2)}%`));
189
195
  // Latency statistics
190
196
  if (metrics.latency && metrics.latency.count > 0) {
191
- console.log("");
192
- console.log(chalk.bold("Latency (ms):"));
193
- console.log(formatRow(" Min:", metrics.latency.min.toFixed(2)));
194
- console.log(formatRow(" Max:", metrics.latency.max.toFixed(2)));
195
- console.log(formatRow(" Mean:", metrics.latency.mean.toFixed(2)));
196
- console.log(formatRow(" P50:", metrics.latency.p50.toFixed(2)));
197
- console.log(formatRow(" P95:", metrics.latency.p95.toFixed(2)));
198
- console.log(formatRow(" P99:", metrics.latency.p99.toFixed(2)));
197
+ logger.always("");
198
+ logger.always(chalk.bold("Latency (ms):"));
199
+ logger.always(formatRow(" Min:", metrics.latency.min.toFixed(2)));
200
+ logger.always(formatRow(" Max:", metrics.latency.max.toFixed(2)));
201
+ logger.always(formatRow(" Mean:", metrics.latency.mean.toFixed(2)));
202
+ logger.always(formatRow(" P50:", metrics.latency.p50.toFixed(2)));
203
+ logger.always(formatRow(" P95:", metrics.latency.p95.toFixed(2)));
204
+ logger.always(formatRow(" P99:", metrics.latency.p99.toFixed(2)));
199
205
  if (args.detailed) {
200
- console.log(formatRow(" P75:", metrics.latency.p75.toFixed(2)));
201
- console.log(formatRow(" P90:", metrics.latency.p90.toFixed(2)));
202
- console.log(formatRow(" Std Dev:", metrics.latency.stdDev.toFixed(2)));
206
+ logger.always(formatRow(" P75:", metrics.latency.p75.toFixed(2)));
207
+ logger.always(formatRow(" P90:", metrics.latency.p90.toFixed(2)));
208
+ logger.always(formatRow(" Std Dev:", metrics.latency.stdDev.toFixed(2)));
203
209
  }
204
210
  }
205
211
  // Token statistics
206
212
  if (metrics.tokens) {
207
- console.log("");
208
- console.log(chalk.bold("Token Usage:"));
209
- console.log(formatRow(" Input tokens:", metrics.tokens.totalInputTokens.toLocaleString()));
210
- console.log(formatRow(" Output tokens:", metrics.tokens.totalOutputTokens.toLocaleString()));
211
- console.log(formatRow(" Total tokens:", metrics.tokens.totalTokens.toLocaleString()));
213
+ logger.always("");
214
+ logger.always(chalk.bold("Token Usage:"));
215
+ logger.always(formatRow(" Input tokens:", metrics.tokens.totalInputTokens.toLocaleString()));
216
+ logger.always(formatRow(" Output tokens:", metrics.tokens.totalOutputTokens.toLocaleString()));
217
+ logger.always(formatRow(" Total tokens:", metrics.tokens.totalTokens.toLocaleString()));
212
218
  if (args.detailed &&
213
219
  metrics.tokens.cacheReadTokens &&
214
220
  metrics.tokens.cacheReadTokens > 0) {
215
- console.log(formatRow(" Cache read:", metrics.tokens.cacheReadTokens.toLocaleString()));
221
+ logger.always(formatRow(" Cache read:", metrics.tokens.cacheReadTokens.toLocaleString()));
216
222
  }
217
223
  if (args.detailed &&
218
224
  metrics.tokens.reasoningTokens &&
219
225
  metrics.tokens.reasoningTokens > 0) {
220
- console.log(formatRow(" Reasoning:", metrics.tokens.reasoningTokens.toLocaleString()));
226
+ logger.always(formatRow(" Reasoning:", metrics.tokens.reasoningTokens.toLocaleString()));
221
227
  }
222
228
  }
223
229
  // Cost
224
230
  if (metrics.totalCost !== undefined && metrics.totalCost > 0) {
225
- console.log("");
226
- console.log(chalk.bold("Cost:"));
227
- console.log(formatRow(" Total:", formatCost(metrics.totalCost)));
231
+ logger.always("");
232
+ logger.always(chalk.bold("Cost:"));
233
+ logger.always(formatRow(" Total:", formatCost(metrics.totalCost)));
228
234
  }
229
235
  // Tracking duration
230
236
  if (metrics.trackingDurationMs) {
231
237
  const durationSec = metrics.trackingDurationMs / 1000;
232
238
  const throughput = metrics.totalSpans > 0 ? metrics.totalSpans / durationSec : 0;
233
- console.log("");
234
- console.log(chalk.gray(`Tracking: ${durationSec.toFixed(1)}s (${throughput.toFixed(2)} req/s)`));
239
+ logger.always("");
240
+ logger.always(chalk.gray(`Tracking: ${durationSec.toFixed(1)}s (${throughput.toFixed(2)} req/s)`));
235
241
  }
236
- console.log("");
242
+ logger.always("");
237
243
  }
238
244
  }
239
245
  catch (error) {
240
- if (spinner)
246
+ if (spinner) {
241
247
  spinner.fail("Failed to get metrics");
248
+ }
242
249
  logger.error("Error:", error instanceof Error ? error.message : String(error));
243
250
  process.exit(1);
244
251
  }
@@ -276,60 +283,62 @@ export class ObservabilityCommandFactory {
276
283
  try {
277
284
  const neurolink = new NeuroLink();
278
285
  const status = neurolink.getTelemetryStatus();
279
- if (spinner)
286
+ if (spinner) {
280
287
  spinner.succeed("Exporters retrieved");
288
+ }
281
289
  const exporters = status.exporters ?? [];
282
290
  if (args.format === "json") {
283
- console.log(JSON.stringify(exporters, null, 2));
291
+ logger.always(JSON.stringify(exporters, null, 2));
284
292
  }
285
293
  else {
286
- console.log("");
287
- console.log(chalk.bold.cyan("=== Configured Exporters ==="));
288
- console.log("");
294
+ logger.always("");
295
+ logger.always(chalk.bold.cyan("=== Configured Exporters ==="));
296
+ logger.always("");
289
297
  if (exporters.length === 0) {
290
- console.log(chalk.gray("No exporters configured."));
291
- console.log("");
292
- console.log("Available exporters:");
293
- console.log(" - Langfuse (langfuse)");
294
- console.log(" - LangSmith (langsmith)");
295
- console.log(" - OpenTelemetry (otel)");
296
- console.log(" - Datadog (datadog)");
297
- console.log(" - Sentry (sentry)");
298
- console.log(" - Braintrust (braintrust)");
299
- console.log(" - Arize (arize)");
300
- console.log(" - PostHog (posthog)");
301
- console.log(" - Laminar (laminar)");
298
+ logger.always(chalk.gray("No exporters configured."));
299
+ logger.always("");
300
+ logger.always("Available exporters:");
301
+ logger.always(" - Langfuse (langfuse)");
302
+ logger.always(" - LangSmith (langsmith)");
303
+ logger.always(" - OpenTelemetry (otel)");
304
+ logger.always(" - Datadog (datadog)");
305
+ logger.always(" - Sentry (sentry)");
306
+ logger.always(" - Braintrust (braintrust)");
307
+ logger.always(" - Arize (arize)");
308
+ logger.always(" - PostHog (posthog)");
309
+ logger.always(" - Laminar (laminar)");
302
310
  }
303
311
  else {
304
312
  for (const exporter of exporters) {
305
313
  const healthIcon = exporter.healthy
306
314
  ? chalk.green("[OK]")
307
315
  : chalk.red("[ERROR]");
308
- console.log(`${healthIcon} ${chalk.bold(exporter.name)}`);
316
+ logger.always(`${healthIcon} ${chalk.bold(exporter.name)}`);
309
317
  if (exporter.latencyMs !== undefined) {
310
- console.log(formatRow(" Latency:", `${exporter.latencyMs.toFixed(2)}ms`));
318
+ logger.always(formatRow(" Latency:", `${exporter.latencyMs.toFixed(2)}ms`));
311
319
  }
312
320
  if (exporter.pendingSpans !== undefined) {
313
- console.log(formatRow(" Pending spans:", exporter.pendingSpans.toLocaleString()));
321
+ logger.always(formatRow(" Pending spans:", exporter.pendingSpans.toLocaleString()));
314
322
  }
315
323
  if (exporter.lastExportTime) {
316
324
  const lastExport = new Date(exporter.lastExportTime);
317
- console.log(formatRow(" Last export:", lastExport.toISOString()));
325
+ logger.always(formatRow(" Last export:", lastExport.toISOString()));
318
326
  }
319
327
  if (exporter.errors && exporter.errors.length > 0) {
320
- console.log(chalk.yellow(" Errors:"));
328
+ logger.always(chalk.yellow(" Errors:"));
321
329
  for (const error of exporter.errors.slice(0, 3)) {
322
- console.log(chalk.red(` - ${error}`));
330
+ logger.always(chalk.red(` - ${error}`));
323
331
  }
324
332
  }
325
- console.log("");
333
+ logger.always("");
326
334
  }
327
335
  }
328
336
  }
329
337
  }
330
338
  catch (error) {
331
- if (spinner)
339
+ if (spinner) {
332
340
  spinner.fail("Failed to get exporters");
341
+ }
333
342
  logger.error("Error:", error instanceof Error ? error.message : String(error));
334
343
  process.exit(1);
335
344
  }
@@ -377,8 +386,9 @@ export class ObservabilityCommandFactory {
377
386
  try {
378
387
  const neurolink = new NeuroLink();
379
388
  const metrics = neurolink.getMetrics();
380
- if (spinner)
389
+ if (spinner) {
381
390
  spinner.succeed("Costs calculated");
391
+ }
382
392
  if (args.format === "json") {
383
393
  const jsonOutput = {
384
394
  totalCost: metrics.totalCost,
@@ -389,60 +399,61 @@ export class ObservabilityCommandFactory {
389
399
  if (args.byModel !== false) {
390
400
  jsonOutput.costByModel = metrics.costByModel;
391
401
  }
392
- console.log(JSON.stringify(jsonOutput, null, 2));
402
+ logger.always(JSON.stringify(jsonOutput, null, 2));
393
403
  }
394
404
  else {
395
- console.log("");
396
- console.log(chalk.bold.cyan("=== Cost Breakdown ==="));
397
- console.log("");
405
+ logger.always("");
406
+ logger.always(chalk.bold.cyan("=== Cost Breakdown ==="));
407
+ logger.always("");
398
408
  // Total cost
399
- console.log(chalk.bold(`Total Cost: ${formatCost(metrics.totalCost ?? 0)}`));
409
+ logger.always(chalk.bold(`Total Cost: ${formatCost(metrics.totalCost ?? 0)}`));
400
410
  // Cost by provider
401
411
  if (args.byProvider !== false &&
402
412
  metrics.costByProvider &&
403
413
  metrics.costByProvider.length > 0) {
404
- console.log("");
405
- console.log(chalk.bold("By Provider:"));
414
+ logger.always("");
415
+ logger.always(chalk.bold("By Provider:"));
406
416
  // Sort by cost descending
407
417
  const sortedProviders = [...metrics.costByProvider].sort((a, b) => b.totalCost - a.totalCost);
408
418
  for (const provider of sortedProviders) {
409
419
  const costStr = formatCost(provider.totalCost);
410
420
  const avgCost = formatCost(provider.avgCostPerRequest);
411
- console.log(` ${chalk.cyan(provider.provider.padEnd(15))} ${costStr}`);
412
- console.log(chalk.gray(` ${provider.requestCount} requests, avg ${avgCost}/req`));
421
+ logger.always(` ${chalk.cyan(provider.provider.padEnd(15))} ${costStr}`);
422
+ logger.always(chalk.gray(` ${provider.requestCount} requests, avg ${avgCost}/req`));
413
423
  }
414
424
  }
415
425
  // Cost by model
416
426
  if (args.byModel !== false &&
417
427
  metrics.costByModel &&
418
428
  metrics.costByModel.length > 0) {
419
- console.log("");
420
- console.log(chalk.bold("By Model:"));
429
+ logger.always("");
430
+ logger.always(chalk.bold("By Model:"));
421
431
  // Sort by cost descending
422
432
  const sortedModels = [...metrics.costByModel].sort((a, b) => b.totalCost - a.totalCost);
423
433
  for (const model of sortedModels) {
424
434
  const costStr = formatCost(model.totalCost);
425
435
  const avgCost = formatCost(model.avgCostPerRequest);
426
- console.log(` ${chalk.cyan(model.model)}`);
427
- console.log(` Cost: ${costStr}`);
428
- console.log(chalk.gray(` ${model.requestCount} requests, avg ${avgCost}/req`));
429
- console.log(chalk.gray(` ${model.inputTokens.toLocaleString()} input, ${model.outputTokens.toLocaleString()} output tokens`));
436
+ logger.always(` ${chalk.cyan(model.model)}`);
437
+ logger.always(` Cost: ${costStr}`);
438
+ logger.always(chalk.gray(` ${model.requestCount} requests, avg ${avgCost}/req`));
439
+ logger.always(chalk.gray(` ${model.inputTokens.toLocaleString()} input, ${model.outputTokens.toLocaleString()} output tokens`));
430
440
  }
431
441
  }
432
442
  // No cost data
433
443
  if ((!metrics.costByProvider ||
434
444
  metrics.costByProvider.length === 0) &&
435
445
  (!metrics.costByModel || metrics.costByModel.length === 0)) {
436
- console.log("");
437
- console.log(chalk.gray("No cost data available."));
438
- console.log(chalk.gray("Cost tracking is recorded when observability is enabled."));
446
+ logger.always("");
447
+ logger.always(chalk.gray("No cost data available."));
448
+ logger.always(chalk.gray("Cost tracking is recorded when observability is enabled."));
439
449
  }
440
- console.log("");
450
+ logger.always("");
441
451
  }
442
452
  }
443
453
  catch (error) {
444
- if (spinner)
454
+ if (spinner) {
445
455
  spinner.fail("Failed to get costs");
456
+ }
446
457
  logger.error("Error:", error instanceof Error ? error.message : String(error));
447
458
  process.exit(1);
448
459
  }
@@ -13,6 +13,32 @@ import type { CommandModule } from "yargs";
13
13
  import type { Hono } from "hono";
14
14
  import type { AccountAllowlist, LoadedProxyConfig, ModelRouterInterface, ProxyGuardArgs, ProxyNeurolinkRuntime, ProxyStartArgs, ProxyStartStrategy, ProxyStatusArgs, ProxyTelemetryArgs, ProxyReadinessState } from "../../lib/types/index.js";
15
15
  import { ProxyRuntimeConfigStore } from "../../lib/proxy/runtimeConfig.js";
16
+ /**
17
+ * Best-effort check that a pid actually belongs to a neurolink proxy process,
18
+ * so a stale/recycled supervisor pid is never mistaken for a live supervisor
19
+ * and signalled. Returns false when the process cannot be confirmed as ours.
20
+ *
21
+ * @param expectedStartTimeIso - The `ProxySupervisorState.startTime` (ISO
22
+ * string) recorded when the supervisor we expect at `pid` was launched.
23
+ * When provided, this is cross-checked against `pid`'s actual OS-reported
24
+ * start time (see {@link getProcessStartTime}) — the args match alone
25
+ * ("neurolink" + "proxy" both present) is not airtight, since an
26
+ * unrelated process (e.g. a shell running this very test suite, or a
27
+ * coincidentally-named script) could match it too. A pid recycled by such
28
+ * a process would have a start time far from the recorded supervisor
29
+ * startTime, which this catches.
30
+ *
31
+ * Deliberately fail-open on anything unparseable: `ps -o lstart=` output
32
+ * parsing is inherently a little fragile (locale/format quirks), and the
33
+ * cost of a false "can't confirm" is silently regressing to pre-hardening
34
+ * behavior, while the cost of a false "confirmed mismatch" is leaving a
35
+ * real supervisor running and orphaning its socket during uninstall — the
36
+ * former is the safer failure mode. So this only ever returns false for
37
+ * the startTime check when BOTH timestamps parsed successfully AND their
38
+ * drift exceeds the tolerance; any parse failure is treated as "can't
39
+ * confirm a mismatch" and falls through to the args-only result.
40
+ */
41
+ export declare function processLooksLikeProxySupervisor(pid: number, expectedStartTimeIso?: string): Promise<boolean>;
16
42
  export declare function mapClaudeErrorTypeToStatus(errorType?: string): number;
17
43
  export declare function createProxyStartApp(params: {
18
44
  neurolink: ProxyNeurolinkRuntime["neurolink"];
@@ -41,6 +41,14 @@ const _require = createRequire(import.meta.url);
41
41
  const PROXY_VERSION = packageJson.version;
42
42
  const PROXY_TELEMETRY_SCRIPT_PATH = fileURLToPath(new URL("../../../scripts/observability/manage-local-openobserve.sh", import.meta.url));
43
43
  const PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS = 5_000;
44
+ // Allowed drift between a pid's OS-reported start time and the persisted
45
+ // ProxySupervisorState.startTime before processLooksLikeProxySupervisor
46
+ // treats it as a confident mismatch (recycled pid). Generous on purpose:
47
+ // `supervisorStartedAt` is captured inside runLaunchdProxySupervisor, which
48
+ // runs after Node has already booted and done setup, so it always lags the
49
+ // OS-level fork/exec by a little — a tight tolerance would produce false
50
+ // mismatches on a slow/cold-started machine.
51
+ const PROXY_SUPERVISOR_START_TIME_TOLERANCE_MS = 10_000;
44
52
  const PROXY_UPDATE_CONTROL_TOKEN = process.env.NEUROLINK_PROXY_UPDATE_CONTROL_TOKEN?.trim() ||
45
53
  crypto.randomUUID();
46
54
  // =============================================================================
@@ -98,23 +106,131 @@ function getProcessStatus(pid) {
98
106
  }
99
107
  }
100
108
  /**
101
- * Best-effort check that a pid actually belongs to a neurolink proxy process,
102
- * so a stale/recycled supervisor pid is never mistaken for a live supervisor
103
- * and signalled. Returns false when the process cannot be confirmed as ours.
109
+ * Read a single `ps -o <field>=` field for `pid`, off the event loop (async
110
+ * `execFile`, not `execFileSync`) and bounded by a short timeout, so a
111
+ * hung/zombie `ps` invocation can never block the event loop or hang the
112
+ * (often uninstall-path) async caller. Shared by {@link getProcessStartTime}
113
+ * and {@link processLooksLikeProxySupervisor}, which both read a single
114
+ * `ps -o <field>=` column for the same pid.
115
+ *
116
+ * Returns null — never throws — on any error, timeout, or empty output, so
117
+ * callers uniformly treat "couldn't read" as "can't confirm either way"
118
+ * rather than distinguishing the failure mode.
104
119
  */
105
- async function processLooksLikeProxySupervisor(pid) {
120
+ async function readPsField(pid, field) {
106
121
  try {
107
- const { execFileSync } = await import("node:child_process");
108
- const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
122
+ const { execFile } = await import("node:child_process");
123
+ const { promisify } = await import("node:util");
124
+ const execFileAsync = promisify(execFile);
125
+ const { stdout } = await withTimeout(execFileAsync("ps", ["-p", String(pid), "-o", `${field}=`], {
109
126
  encoding: "utf-8",
110
- stdio: ["ignore", "pipe", "ignore"],
111
- });
112
- return /neurolink/i.test(args);
127
+ }), 2000, `ps -o ${field}= timed out for pid ${pid}`);
128
+ const trimmed = stdout.trim();
129
+ return trimmed || null;
113
130
  }
114
131
  catch {
115
- // `ps` unavailable or the pid vanished — do not signal an unverified pid.
132
+ return null;
133
+ }
134
+ }
135
+ /**
136
+ * Best-effort, macOS-only (uninstall is macOS-only) read of `pid`'s
137
+ * OS-reported start time via `ps -o lstart=`, which prints a fixed-width
138
+ * `Www Mmm dd hh:mm:ss yyyy` timestamp — the same shape `Date`'s parser
139
+ * already understands (it's the format `Date#toString()` itself produces
140
+ * modulo the trailing timezone). Returns null (never throws) when `ps`
141
+ * fails or its output doesn't parse, so callers can tell "confirmed
142
+ * mismatch" apart from "couldn't confirm either way".
143
+ */
144
+ async function getProcessStartTime(pid) {
145
+ const out = await readPsField(pid, "lstart");
146
+ if (!out) {
147
+ return null;
148
+ }
149
+ // `ps -o lstart=` prints the OS start time in the machine's LOCAL timezone
150
+ // with no offset, e.g. "Sun Jul 19 20:12:41 2026". Parse the components and
151
+ // build the Date via the local-time constructor rather than relying on the
152
+ // engine's implementation-defined parsing of non-ISO date strings. The
153
+ // result is the same absolute instant the supervisor persisted as an
154
+ // ISO-UTC string, so the drift comparison against it is timezone-safe.
155
+ const m = out.match(/^\w{3}\s+(\w{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/);
156
+ if (!m) {
157
+ return null;
158
+ }
159
+ const monthIndex = [
160
+ "Jan",
161
+ "Feb",
162
+ "Mar",
163
+ "Apr",
164
+ "May",
165
+ "Jun",
166
+ "Jul",
167
+ "Aug",
168
+ "Sep",
169
+ "Oct",
170
+ "Nov",
171
+ "Dec",
172
+ ].indexOf(m[1]);
173
+ if (monthIndex < 0) {
174
+ return null;
175
+ }
176
+ const parsed = new Date(Number(m[6]), monthIndex, Number(m[2]), Number(m[3]), Number(m[4]), Number(m[5]));
177
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
178
+ }
179
+ /**
180
+ * Best-effort check that a pid actually belongs to a neurolink proxy process,
181
+ * so a stale/recycled supervisor pid is never mistaken for a live supervisor
182
+ * and signalled. Returns false when the process cannot be confirmed as ours.
183
+ *
184
+ * @param expectedStartTimeIso - The `ProxySupervisorState.startTime` (ISO
185
+ * string) recorded when the supervisor we expect at `pid` was launched.
186
+ * When provided, this is cross-checked against `pid`'s actual OS-reported
187
+ * start time (see {@link getProcessStartTime}) — the args match alone
188
+ * ("neurolink" + "proxy" both present) is not airtight, since an
189
+ * unrelated process (e.g. a shell running this very test suite, or a
190
+ * coincidentally-named script) could match it too. A pid recycled by such
191
+ * a process would have a start time far from the recorded supervisor
192
+ * startTime, which this catches.
193
+ *
194
+ * Deliberately fail-open on anything unparseable: `ps -o lstart=` output
195
+ * parsing is inherently a little fragile (locale/format quirks), and the
196
+ * cost of a false "can't confirm" is silently regressing to pre-hardening
197
+ * behavior, while the cost of a false "confirmed mismatch" is leaving a
198
+ * real supervisor running and orphaning its socket during uninstall — the
199
+ * former is the safer failure mode. So this only ever returns false for
200
+ * the startTime check when BOTH timestamps parsed successfully AND their
201
+ * drift exceeds the tolerance; any parse failure is treated as "can't
202
+ * confirm a mismatch" and falls through to the args-only result.
203
+ */
204
+ export async function processLooksLikeProxySupervisor(pid, expectedStartTimeIso) {
205
+ const args = await readPsField(pid, "args");
206
+ if (args === null) {
207
+ // `ps` unavailable, timed out, or the pid vanished — do not signal an
208
+ // unverified pid.
209
+ return false;
210
+ }
211
+ // `/neurolink/i` alone is too broad: any process whose args merely
212
+ // *mention* "neurolink" (e.g. a shell running in a directory named
213
+ // "neurolink", an editor with a neurolink file open) would match, and a
214
+ // stale/recycled pid running such a process could then be SIGTERM'd/
215
+ // SIGKILL'd by `ensureSupervisorStoppedBeforeClear` during uninstall.
216
+ // Require the actual proxy-supervisor invocation — both "neurolink" AND
217
+ // the "proxy" subcommand present in args.
218
+ if (!(/neurolink/i.test(args) && /\bproxy\b/i.test(args))) {
116
219
  return false;
117
220
  }
221
+ if (!expectedStartTimeIso) {
222
+ return true;
223
+ }
224
+ const expected = new Date(expectedStartTimeIso);
225
+ if (Number.isNaN(expected.getTime())) {
226
+ return true; // recorded startTime itself is unparseable — can't confirm a mismatch
227
+ }
228
+ const actual = await getProcessStartTime(pid);
229
+ if (!actual) {
230
+ return true; // ps -o lstart= unavailable/unparseable — can't confirm a mismatch
231
+ }
232
+ const driftMs = Math.abs(actual.getTime() - expected.getTime());
233
+ return driftMs <= PROXY_SUPERVISOR_START_TIME_TOLERANCE_MS;
118
234
  }
119
235
  /**
120
236
  * Ensure any supervisor recorded in state is actually stopped before its state
@@ -124,14 +240,17 @@ async function processLooksLikeProxySupervisor(pid) {
124
240
  * the uninstall instead of silently discarding the record.
125
241
  */
126
242
  async function ensureSupervisorStoppedBeforeClear() {
127
- const pid = loadProxySupervisorState()?.pid;
243
+ const supervisorState = loadProxySupervisorState();
244
+ const pid = supervisorState?.pid;
128
245
  if (!pid || getProcessStatus(pid) === "not_running") {
129
246
  return true;
130
247
  }
131
248
  // Guard against a stale/recycled pid: only signal a process that still looks
132
249
  // like a neurolink proxy supervisor, so uninstall never terminates an
133
- // unrelated, same-user process that inherited the recorded pid.
134
- if (!(await processLooksLikeProxySupervisor(pid))) {
250
+ // unrelated, same-user process that inherited the recorded pid. Also cross-
251
+ // checks the pid's actual OS start time against the recorded
252
+ // supervisorState.startTime (see processLooksLikeProxySupervisor).
253
+ if (!(await processLooksLikeProxySupervisor(pid, supervisorState?.startTime))) {
135
254
  return true;
136
255
  }
137
256
  try {