@juspay/neurolink 9.34.0 → 9.36.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 CHANGED
@@ -1,3 +1,15 @@
1
+ ## [9.36.0](https://github.com/juspay/neurolink/compare/v9.35.0...v9.36.0) (2026-03-28)
2
+
3
+ ### Features
4
+
5
+ - **(proxy):** add auto-update with traffic-aware graceful restart ([4a11a78](https://github.com/juspay/neurolink/commit/4a11a783adb4424d7a303b298c6cf9989cd4ed63))
6
+
7
+ ## [9.35.0](https://github.com/juspay/neurolink/compare/v9.34.0...v9.35.0) (2026-03-28)
8
+
9
+ ### Features
10
+
11
+ - **(proxy):** add auto-update with traffic-aware graceful restart ([e0ea718](https://github.com/juspay/neurolink/commit/e0ea71891f51cf3d0f1f4fbc0044efc839359eaf))
12
+
1
13
  ## [9.34.0](https://github.com/juspay/neurolink/compare/v9.33.0...v9.34.0) (2026-03-27)
2
14
 
3
15
  ### Features
@@ -16,6 +16,9 @@ import chalk from "chalk";
16
16
  import ora from "ora";
17
17
  import { logger } from "../../lib/utils/logger.js";
18
18
  import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
19
+ import { createRequire } from "node:module";
20
+ const _require = createRequire(import.meta.url);
21
+ const { version: PROXY_VERSION } = _require("../../../package.json");
19
22
  // =============================================================================
20
23
  // STATE MANAGEMENT
21
24
  // =============================================================================
@@ -193,7 +196,7 @@ function spawnFailOpenGuard(host, port, parentPid) {
193
196
  return undefined;
194
197
  }
195
198
  try {
196
- const child = spawn(process.execPath, [
199
+ const args = [
197
200
  entryScript,
198
201
  "proxy",
199
202
  "guard",
@@ -204,7 +207,8 @@ function spawnFailOpenGuard(host, port, parentPid) {
204
207
  "--parent-pid",
205
208
  String(parentPid),
206
209
  "--quiet",
207
- ], {
210
+ ];
211
+ const child = spawn(process.execPath, args, {
208
212
  detached: true,
209
213
  stdio: "ignore",
210
214
  });
@@ -545,6 +549,7 @@ export const proxyStartCommand = {
545
549
  status: "ok",
546
550
  strategy,
547
551
  uptime: process.uptime(),
552
+ version: PROXY_VERSION,
548
553
  }));
549
554
  // Status endpoint (detailed)
550
555
  app.get("/status", async (c) => {
@@ -557,6 +562,7 @@ export const proxyStartCommand = {
557
562
  host,
558
563
  strategy,
559
564
  uptime: process.uptime(),
565
+ version: PROXY_VERSION,
560
566
  stats: {
561
567
  totalRequests: stats.totalRequests,
562
568
  totalSuccess: stats.totalSuccess,
@@ -921,6 +927,153 @@ export const proxyGuardCommand = {
921
927
  if (!Number.isFinite(parentPid) || parentPid <= 0) {
922
928
  return;
923
929
  }
930
+ // ---------------------------------------------------------------
931
+ // Auto-update loop (runs concurrently with the health monitor)
932
+ // Always on — no flags needed. Hardcoded sensible defaults.
933
+ // ---------------------------------------------------------------
934
+ const UPDATE_CHECK_INTERVAL_MS = 2 * 60 * 60 * 1000; // 2 hours
935
+ const QUIET_THRESHOLD_MS = 120 * 1000; // 2 minutes of silence
936
+ const UPDATE_TIMEOUT_MS = 30 * 1000; // 30 seconds to come healthy
937
+ // Get running version from /health endpoint (with timeout to avoid hanging)
938
+ let runningVersion = PROXY_VERSION; // fallback
939
+ try {
940
+ const healthResp = await fetch(`http://${host}:${port}/health`, {
941
+ signal: AbortSignal.timeout(5_000),
942
+ });
943
+ const healthData = (await healthResp.json());
944
+ runningVersion = healthData.version ?? PROXY_VERSION;
945
+ }
946
+ catch {
947
+ /* use fallback */
948
+ }
949
+ // Auto-update only works on macOS with launchd. On other platforms,
950
+ // there's no restart mechanism, so skip the update loop entirely.
951
+ const canAutoUpdate = process.platform === "darwin" && (await isLaunchdManaging());
952
+ let updateInProgress = false;
953
+ let updateRestartInProgress = false;
954
+ const runUpdateCheck = async () => {
955
+ if (updateInProgress) {
956
+ return;
957
+ }
958
+ updateInProgress = true;
959
+ try {
960
+ // Lazy-load update modules so they're only imported at check time
961
+ const { checkForUpdate } = await import("../../lib/proxy/updateChecker.js");
962
+ const { checkTrafficQuiet } = await import("../../lib/proxy/quietDetector.js");
963
+ const { recordCheck, isVersionSuppressed, suppressVersion, recordSuccessfulUpdate, } = await import("../../lib/proxy/updateState.js");
964
+ // 1. Check for update
965
+ const result = await checkForUpdate(runningVersion);
966
+ recordCheck(result.latestVersion);
967
+ if (!result.updateAvailable) {
968
+ return;
969
+ }
970
+ if (isVersionSuppressed(result.latestVersion)) {
971
+ logger.debug(`[guard] version ${result.latestVersion} is suppressed, skipping`);
972
+ return;
973
+ }
974
+ logger.always(`[guard] update available: ${runningVersion} → ${result.latestVersion}`);
975
+ // 2. Wait for quiet traffic
976
+ const maxQuietWaitMs = 60 * 60 * 1000; // 1 hour max wait
977
+ const quietPollMs = 10_000; // check every 10s
978
+ const quietStart = Date.now();
979
+ while (Date.now() - quietStart < maxQuietWaitMs) {
980
+ // Bail out if parent proxy died during the wait
981
+ if (getProcessStatus(parentPid) === "not_running") {
982
+ logger.always(`[guard] parent process died during quiet-wait, aborting update`);
983
+ return;
984
+ }
985
+ const quietStatus = checkTrafficQuiet(QUIET_THRESHOLD_MS);
986
+ if (quietStatus.isQuiet) {
987
+ break;
988
+ }
989
+ logger.debug(`[guard] traffic active (last activity ${Math.round(quietStatus.silenceDurationMs / 1000)}s ago), waiting...`);
990
+ await new Promise((r) => setTimeout(r, quietPollMs));
991
+ }
992
+ const finalQuiet = checkTrafficQuiet(QUIET_THRESHOLD_MS);
993
+ if (!finalQuiet.isQuiet) {
994
+ logger.always(`[guard] traffic didn't quiet down within 1 hour, skipping update cycle`);
995
+ return;
996
+ }
997
+ // 3. Install update (validate version string before passing to shell)
998
+ if (!/^\d+\.\d+\.\d+$/.test(result.latestVersion)) {
999
+ logger.always(`[guard] WARNING: invalid version format "${result.latestVersion}", skipping`);
1000
+ return;
1001
+ }
1002
+ logger.always(`[guard] traffic quiet, installing @juspay/neurolink@${result.latestVersion}...`);
1003
+ const { execFileSync } = await import("node:child_process");
1004
+ try {
1005
+ execFileSync("pnpm", ["add", "-g", `@juspay/neurolink@${result.latestVersion}`], {
1006
+ timeout: 120_000,
1007
+ stdio: "pipe",
1008
+ });
1009
+ }
1010
+ catch (installErr) {
1011
+ logger.always(`[guard] WARNING: pnpm install failed: ${installErr instanceof Error ? installErr.message : String(installErr)}`);
1012
+ suppressVersion(result.latestVersion, "install_failed");
1013
+ return;
1014
+ }
1015
+ // 4. Restart via launchctl
1016
+ // Signal the health loop to not exit when it detects
1017
+ // the parent PID is gone — we're intentionally restarting.
1018
+ updateRestartInProgress = true;
1019
+ logger.always(`[guard] restarting proxy via launchctl...`);
1020
+ const uid = process.getuid?.() ?? 501;
1021
+ try {
1022
+ execFileSync("launchctl", ["kickstart", "-k", `gui/${uid}/${PLIST_LABEL}`], {
1023
+ timeout: 10_000,
1024
+ stdio: "pipe",
1025
+ });
1026
+ }
1027
+ catch {
1028
+ logger.always(`[guard] WARNING: launchctl kickstart failed`);
1029
+ suppressVersion(result.latestVersion, "restart_failed");
1030
+ return;
1031
+ }
1032
+ // 5. Wait for healthy restart
1033
+ let healthy = false;
1034
+ const restartStart = Date.now();
1035
+ while (Date.now() - restartStart < UPDATE_TIMEOUT_MS) {
1036
+ await new Promise((r) => setTimeout(r, 2000));
1037
+ try {
1038
+ const resp = await fetch(`http://${host}:${port}/health`, {
1039
+ signal: AbortSignal.timeout(3000),
1040
+ });
1041
+ if (resp.ok) {
1042
+ const data = (await resp.json());
1043
+ if (data.version === result.latestVersion) {
1044
+ healthy = true;
1045
+ break;
1046
+ }
1047
+ }
1048
+ }
1049
+ catch {
1050
+ /* retry */
1051
+ }
1052
+ }
1053
+ if (healthy) {
1054
+ logger.always(`[guard] update successful: now running ${result.latestVersion}`);
1055
+ recordSuccessfulUpdate(result.latestVersion);
1056
+ // The new proxy will spawn its own guard. Exit this one.
1057
+ process.exit(0);
1058
+ }
1059
+ else {
1060
+ logger.always(`[guard] WARNING: proxy unhealthy after update to ${result.latestVersion}`);
1061
+ suppressVersion(result.latestVersion, "unhealthy_after_restart");
1062
+ updateRestartInProgress = false;
1063
+ }
1064
+ }
1065
+ catch (err) {
1066
+ logger.always(`[guard] update check error: ${err instanceof Error ? err.message : String(err)}`);
1067
+ }
1068
+ finally {
1069
+ updateInProgress = false;
1070
+ }
1071
+ };
1072
+ // Run first check after a short delay, then on interval
1073
+ if (canAutoUpdate) {
1074
+ setTimeout(runUpdateCheck, 30_000);
1075
+ setInterval(runUpdateCheck, UPDATE_CHECK_INTERVAL_MS);
1076
+ }
924
1077
  const startedAt = Date.now();
925
1078
  let parentStatus = getProcessStatus(parentPid);
926
1079
  let consecutiveUnhealthy = 0;
@@ -933,8 +1086,9 @@ export const proxyGuardCommand = {
933
1086
  else {
934
1087
  consecutiveUnhealthy += 1;
935
1088
  }
936
- if (parentStatus === "not_running") {
937
- // Parent is gone. If endpoint is still healthy, another proxy took over.
1089
+ if (parentStatus === "not_running" && !updateRestartInProgress) {
1090
+ // Parent is gone (and we're not mid-update-restart).
1091
+ // If endpoint is still healthy, another proxy took over.
938
1092
  if (healthy) {
939
1093
  return;
940
1094
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Proxy Quiet Detector
3
+ * Determines whether the proxy has been idle (no traffic) for a given
4
+ * threshold by efficiently reading only the tail of today's debug log file.
5
+ * Used by the auto-update system to find safe windows for restarts.
6
+ */
7
+ /** Result of a traffic-quiet check. */
8
+ export interface QuietStatus {
9
+ isQuiet: boolean;
10
+ lastActivityAt: Date | null;
11
+ silenceDurationMs: number;
12
+ }
13
+ /**
14
+ * Check whether proxy traffic has been quiet (no requests) for at least
15
+ * `quietThresholdMs` milliseconds.
16
+ *
17
+ * Reads only the tail of today's debug log file for efficiency.
18
+ *
19
+ * @param quietThresholdMs Silence duration (ms) to consider "quiet". Default: 120 000 (2 min).
20
+ * @returns QuietStatus with the idle analysis.
21
+ */
22
+ export declare function checkTrafficQuiet(quietThresholdMs?: number): QuietStatus;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Proxy Quiet Detector
3
+ * Determines whether the proxy has been idle (no traffic) for a given
4
+ * threshold by efficiently reading only the tail of today's debug log file.
5
+ * Used by the auto-update system to find safe windows for restarts.
6
+ */
7
+ import { openSync, readSync, closeSync, fstatSync, existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { homedir } from "node:os";
10
+ /** Default quiet threshold: 2 minutes of no traffic. */
11
+ const DEFAULT_QUIET_THRESHOLD_MS = 120_000;
12
+ /** Maximum bytes to read from the tail of the log file. */
13
+ const TAIL_READ_SIZE = 4096;
14
+ /**
15
+ * Build the path to a proxy debug log file for a given date.
16
+ * Format: ~/.neurolink/logs/proxy-debug-YYYY-MM-DD.jsonl
17
+ */
18
+ function getLogPathForDate(date) {
19
+ const dateStr = date.toISOString().split("T")[0];
20
+ return join(homedir(), ".neurolink", "logs", `proxy-debug-${dateStr}.jsonl`);
21
+ }
22
+ /**
23
+ * Get the most relevant log file path. Uses today's log if it exists,
24
+ * otherwise falls back to yesterday's to handle the midnight rollover case
25
+ * (last request at 23:59, update check at 00:01).
26
+ */
27
+ function getActiveLogPath() {
28
+ const todayPath = getLogPathForDate(new Date());
29
+ if (existsSync(todayPath)) {
30
+ return todayPath;
31
+ }
32
+ const yesterday = new Date(Date.now() - 86_400_000);
33
+ return getLogPathForDate(yesterday);
34
+ }
35
+ /**
36
+ * Read the last complete line(s) from a file efficiently.
37
+ * Uses low-level fs to seek to end and read only the last TAIL_READ_SIZE bytes.
38
+ * Returns an array of the last non-empty lines (up to 2 for fallback).
39
+ */
40
+ function readTailLines(filePath) {
41
+ let fd = null;
42
+ try {
43
+ fd = openSync(filePath, "r");
44
+ const stat = fstatSync(fd);
45
+ if (stat.size === 0) {
46
+ return [];
47
+ }
48
+ const readSize = Math.min(TAIL_READ_SIZE, stat.size);
49
+ const offset = stat.size - readSize;
50
+ const buffer = Buffer.alloc(readSize);
51
+ readSync(fd, buffer, 0, readSize, offset);
52
+ const chunk = buffer.toString("utf-8");
53
+ // Split into lines, filter out empty trailing entries
54
+ const lines = chunk.split("\n").filter((line) => line.trim().length > 0);
55
+ // Return last 2 lines (last + fallback)
56
+ return lines.slice(-2);
57
+ }
58
+ finally {
59
+ if (fd !== null) {
60
+ closeSync(fd);
61
+ }
62
+ }
63
+ }
64
+ /**
65
+ * Try to parse a JSON line and extract its ISO timestamp.
66
+ * Returns the timestamp as epoch ms, or null if parsing fails.
67
+ */
68
+ function extractTimestamp(line) {
69
+ try {
70
+ const parsed = JSON.parse(line);
71
+ if (typeof parsed.timestamp === "string") {
72
+ const ms = Date.parse(parsed.timestamp);
73
+ if (!Number.isNaN(ms)) {
74
+ return ms;
75
+ }
76
+ }
77
+ }
78
+ catch {
79
+ // Malformed JSON — caller will handle fallback
80
+ }
81
+ return null;
82
+ }
83
+ /**
84
+ * Check whether proxy traffic has been quiet (no requests) for at least
85
+ * `quietThresholdMs` milliseconds.
86
+ *
87
+ * Reads only the tail of today's debug log file for efficiency.
88
+ *
89
+ * @param quietThresholdMs Silence duration (ms) to consider "quiet". Default: 120 000 (2 min).
90
+ * @returns QuietStatus with the idle analysis.
91
+ */
92
+ export function checkTrafficQuiet(quietThresholdMs = DEFAULT_QUIET_THRESHOLD_MS) {
93
+ const noActivityResult = {
94
+ isQuiet: true,
95
+ lastActivityAt: null,
96
+ silenceDurationMs: Infinity,
97
+ };
98
+ const logPath = getActiveLogPath();
99
+ if (!existsSync(logPath)) {
100
+ return noActivityResult;
101
+ }
102
+ const tailLines = readTailLines(logPath);
103
+ if (tailLines.length === 0) {
104
+ return noActivityResult;
105
+ }
106
+ // Try last line first, then fall back to the one before it
107
+ let timestampMs = null;
108
+ for (let i = tailLines.length - 1; i >= 0; i--) {
109
+ timestampMs = extractTimestamp(tailLines[i]);
110
+ if (timestampMs !== null) {
111
+ break;
112
+ }
113
+ }
114
+ if (timestampMs === null) {
115
+ // All tail lines are malformed — treat as quiet
116
+ return noActivityResult;
117
+ }
118
+ const silenceDurationMs = Date.now() - timestampMs;
119
+ return {
120
+ isQuiet: silenceDurationMs >= quietThresholdMs,
121
+ lastActivityAt: new Date(timestampMs),
122
+ silenceDurationMs,
123
+ };
124
+ }
125
+ //# sourceMappingURL=quietDetector.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Proxy auto-update version checker.
3
+ *
4
+ * Queries the npm registry for the latest published version of
5
+ * `@juspay/neurolink` and compares it against the currently running version.
6
+ * Designed to be non-blocking and failure-tolerant — any error (network,
7
+ * timeout, parse) silently returns `updateAvailable: false`.
8
+ */
9
+ export interface UpdateCheckResult {
10
+ currentVersion: string;
11
+ latestVersion: string;
12
+ updateAvailable: boolean;
13
+ }
14
+ /**
15
+ * Query npm for the latest version of `@juspay/neurolink` and compare it
16
+ * against {@link currentVersion}.
17
+ *
18
+ * On **any** failure the function resolves (never rejects) with
19
+ * `{ updateAvailable: false, latestVersion: currentVersion }`.
20
+ */
21
+ export declare function checkForUpdate(currentVersion: string): Promise<UpdateCheckResult>;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Proxy auto-update version checker.
3
+ *
4
+ * Queries the npm registry for the latest published version of
5
+ * `@juspay/neurolink` and compares it against the currently running version.
6
+ * Designed to be non-blocking and failure-tolerant — any error (network,
7
+ * timeout, parse) silently returns `updateAvailable: false`.
8
+ */
9
+ import { execFile as execFileCb } from "node:child_process";
10
+ import { promisify } from "node:util";
11
+ import { logger } from "../utils/logger.js";
12
+ const execFile = promisify(execFileCb);
13
+ /** Timeout (ms) for the `npm view` child process. */
14
+ const NPM_VIEW_TIMEOUT_MS = 10_000;
15
+ /**
16
+ * Parse a version string of the form `major.minor.patch` into numeric
17
+ * components. Returns `null` when the string does not match.
18
+ */
19
+ function parseSemVer(version) {
20
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
21
+ if (!match) {
22
+ return null;
23
+ }
24
+ return {
25
+ major: Number(match[1]),
26
+ minor: Number(match[2]),
27
+ patch: Number(match[3]),
28
+ };
29
+ }
30
+ /**
31
+ * Returns `true` when `latest` is strictly greater than `current`.
32
+ *
33
+ * Both arguments must be valid semver strings; returns `false` on any
34
+ * parse failure so the caller never sees a spurious "update available".
35
+ */
36
+ function isNewerVersion(current, latest) {
37
+ const cur = parseSemVer(current);
38
+ const lat = parseSemVer(latest);
39
+ if (!cur || !lat) {
40
+ return false;
41
+ }
42
+ if (lat.major !== cur.major) {
43
+ return lat.major > cur.major;
44
+ }
45
+ if (lat.minor !== cur.minor) {
46
+ return lat.minor > cur.minor;
47
+ }
48
+ return lat.patch > cur.patch;
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // Core check
52
+ // ---------------------------------------------------------------------------
53
+ /**
54
+ * Query npm for the latest version of `@juspay/neurolink` and compare it
55
+ * against {@link currentVersion}.
56
+ *
57
+ * On **any** failure the function resolves (never rejects) with
58
+ * `{ updateAvailable: false, latestVersion: currentVersion }`.
59
+ */
60
+ export async function checkForUpdate(currentVersion) {
61
+ const fail = {
62
+ currentVersion,
63
+ latestVersion: currentVersion,
64
+ updateAvailable: false,
65
+ };
66
+ try {
67
+ logger.debug("[UpdateChecker] Checking for updates", { currentVersion });
68
+ const { stdout } = await execFile("npm", ["view", "@juspay/neurolink", "version", "--json"], { timeout: NPM_VIEW_TIMEOUT_MS });
69
+ // `npm view ... --json` wraps the value in double-quotes, e.g. `"9.32.0"`
70
+ const parsed = JSON.parse(stdout);
71
+ if (typeof parsed !== "string") {
72
+ logger.warn("[UpdateChecker] Unexpected npm output type", {
73
+ type: typeof parsed,
74
+ });
75
+ return fail;
76
+ }
77
+ const latestVersion = parsed.trim();
78
+ if (!parseSemVer(latestVersion)) {
79
+ logger.warn("[UpdateChecker] Failed to parse latest version", {
80
+ latestVersion,
81
+ });
82
+ return fail;
83
+ }
84
+ const updateAvailable = isNewerVersion(currentVersion, latestVersion);
85
+ logger.debug("[UpdateChecker] Version check complete", {
86
+ currentVersion,
87
+ latestVersion,
88
+ updateAvailable,
89
+ });
90
+ return { currentVersion, latestVersion, updateAvailable };
91
+ }
92
+ catch (error) {
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ logger.warn("[UpdateChecker] Update check failed", { error: message });
95
+ return fail;
96
+ }
97
+ }
98
+ //# sourceMappingURL=updateChecker.js.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Update State Persistence
3
+ * Manages persistent state for the proxy auto-update feature.
4
+ * Tracks check timestamps, suppressed versions, and update history.
5
+ *
6
+ * State file location: ~/.neurolink/update-state.json
7
+ * Suppressed versions expire after 24 hours.
8
+ */
9
+ export interface SuppressedVersion {
10
+ suppressedAt: string;
11
+ reason: string;
12
+ }
13
+ export interface UpdateState {
14
+ lastCheckAt: string;
15
+ lastCheckVersion: string;
16
+ suppressedVersions: Record<string, SuppressedVersion>;
17
+ lastUpdateAt: string | null;
18
+ lastUpdateVersion: string | null;
19
+ }
20
+ /**
21
+ * Return an empty/initial UpdateState.
22
+ */
23
+ export declare function getDefaultUpdateState(): UpdateState;
24
+ /**
25
+ * Load the update state from disk.
26
+ * Returns null if the file does not exist.
27
+ * Returns the default state if the file contains corrupt JSON.
28
+ *
29
+ * @param stateFilePath - Override path for testing (default: ~/.neurolink/update-state.json)
30
+ */
31
+ export declare function loadUpdateState(stateFilePath?: string): UpdateState | null;
32
+ /**
33
+ * Save the update state to disk.
34
+ *
35
+ * @param state - The UpdateState to persist
36
+ * @param stateFilePath - Override path for testing (default: ~/.neurolink/update-state.json)
37
+ */
38
+ export declare function saveUpdateState(state: UpdateState, stateFilePath?: string): void;
39
+ /**
40
+ * Check whether a version is currently suppressed (i.e., suppressed AND within the 24-hour window).
41
+ *
42
+ * @param version - Semver version string to check
43
+ * @param stateFilePath - Override path for testing
44
+ */
45
+ export declare function isVersionSuppressed(version: string, stateFilePath?: string): boolean;
46
+ /**
47
+ * Add a version to the suppressed list and persist.
48
+ *
49
+ * @param version - Semver version string to suppress
50
+ * @param reason - Human-readable reason for suppression
51
+ * @param stateFilePath - Override path for testing
52
+ */
53
+ export declare function suppressVersion(version: string, reason: string, stateFilePath?: string): void;
54
+ /**
55
+ * Record a successful update: set lastUpdateAt and lastUpdateVersion, then persist.
56
+ *
57
+ * @param version - The version that was successfully installed
58
+ * @param stateFilePath - Override path for testing
59
+ */
60
+ export declare function recordSuccessfulUpdate(version: string, stateFilePath?: string): void;
61
+ /**
62
+ * Record an update check: set lastCheckAt and lastCheckVersion, then persist.
63
+ *
64
+ * @param latestVersion - The latest version found during the check
65
+ * @param stateFilePath - Override path for testing
66
+ */
67
+ export declare function recordCheck(latestVersion: string, stateFilePath?: string): void;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Update State Persistence
3
+ * Manages persistent state for the proxy auto-update feature.
4
+ * Tracks check timestamps, suppressed versions, and update history.
5
+ *
6
+ * State file location: ~/.neurolink/update-state.json
7
+ * Suppressed versions expire after 24 hours.
8
+ */
9
+ import fs from "fs";
10
+ import os from "os";
11
+ import path from "path";
12
+ // ============================================
13
+ // Constants
14
+ // ============================================
15
+ const STATE_FILENAME = "update-state.json";
16
+ const SUPPRESSION_TTL_MS = 86_400_000; // 24 hours
17
+ // ============================================
18
+ // Internal Helpers
19
+ // ============================================
20
+ /**
21
+ * Resolve the path to the update state file.
22
+ * Accepts an override for testing; defaults to ~/.neurolink/update-state.json.
23
+ */
24
+ function resolveStatePath(overridePath) {
25
+ if (overridePath) {
26
+ return overridePath;
27
+ }
28
+ return path.join(os.homedir(), ".neurolink", STATE_FILENAME);
29
+ }
30
+ /**
31
+ * Ensure the parent directory of the given file path exists.
32
+ */
33
+ function ensureParentDir(filePath) {
34
+ const dir = path.dirname(filePath);
35
+ if (!fs.existsSync(dir)) {
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ }
38
+ }
39
+ // ============================================
40
+ // Exported Functions
41
+ // ============================================
42
+ /**
43
+ * Return an empty/initial UpdateState.
44
+ */
45
+ export function getDefaultUpdateState() {
46
+ return {
47
+ lastCheckAt: new Date(0).toISOString(),
48
+ lastCheckVersion: "",
49
+ suppressedVersions: {},
50
+ lastUpdateAt: null,
51
+ lastUpdateVersion: null,
52
+ };
53
+ }
54
+ /**
55
+ * Load the update state from disk.
56
+ * Returns null if the file does not exist.
57
+ * Returns the default state if the file contains corrupt JSON.
58
+ *
59
+ * @param stateFilePath - Override path for testing (default: ~/.neurolink/update-state.json)
60
+ */
61
+ export function loadUpdateState(stateFilePath) {
62
+ const filePath = resolveStatePath(stateFilePath);
63
+ try {
64
+ if (!fs.existsSync(filePath)) {
65
+ return null;
66
+ }
67
+ const content = fs.readFileSync(filePath, "utf8");
68
+ const parsed = JSON.parse(content);
69
+ // Minimal shape check — reject valid JSON that isn't an UpdateState
70
+ // Note: typeof null === "object", so we check both
71
+ if (typeof parsed !== "object" ||
72
+ parsed === null ||
73
+ typeof parsed.suppressedVersions !== "object" ||
74
+ parsed.suppressedVersions === null ||
75
+ Array.isArray(parsed.suppressedVersions) ||
76
+ typeof parsed.lastCheckAt !== "string") {
77
+ return getDefaultUpdateState();
78
+ }
79
+ return parsed;
80
+ }
81
+ catch {
82
+ // Corrupt or unreadable JSON — return default state
83
+ return getDefaultUpdateState();
84
+ }
85
+ }
86
+ /**
87
+ * Save the update state to disk.
88
+ *
89
+ * @param state - The UpdateState to persist
90
+ * @param stateFilePath - Override path for testing (default: ~/.neurolink/update-state.json)
91
+ */
92
+ export function saveUpdateState(state, stateFilePath) {
93
+ const filePath = resolveStatePath(stateFilePath);
94
+ ensureParentDir(filePath);
95
+ // Atomic write: write to temp file then rename to prevent corruption on crash
96
+ const tmpPath = filePath + ".tmp";
97
+ fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
98
+ fs.renameSync(tmpPath, filePath);
99
+ }
100
+ /**
101
+ * Check whether a version is currently suppressed (i.e., suppressed AND within the 24-hour window).
102
+ *
103
+ * @param version - Semver version string to check
104
+ * @param stateFilePath - Override path for testing
105
+ */
106
+ export function isVersionSuppressed(version, stateFilePath) {
107
+ const state = loadUpdateState(stateFilePath);
108
+ if (!state) {
109
+ return false;
110
+ }
111
+ const entry = state.suppressedVersions[version];
112
+ if (!entry) {
113
+ return false;
114
+ }
115
+ return Date.now() - Date.parse(entry.suppressedAt) < SUPPRESSION_TTL_MS;
116
+ }
117
+ /**
118
+ * Add a version to the suppressed list and persist.
119
+ *
120
+ * @param version - Semver version string to suppress
121
+ * @param reason - Human-readable reason for suppression
122
+ * @param stateFilePath - Override path for testing
123
+ */
124
+ export function suppressVersion(version, reason, stateFilePath) {
125
+ const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
126
+ state.suppressedVersions[version] = {
127
+ suppressedAt: new Date().toISOString(),
128
+ reason,
129
+ };
130
+ saveUpdateState(state, stateFilePath);
131
+ }
132
+ /**
133
+ * Record a successful update: set lastUpdateAt and lastUpdateVersion, then persist.
134
+ *
135
+ * @param version - The version that was successfully installed
136
+ * @param stateFilePath - Override path for testing
137
+ */
138
+ export function recordSuccessfulUpdate(version, stateFilePath) {
139
+ const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
140
+ state.lastUpdateAt = new Date().toISOString();
141
+ state.lastUpdateVersion = version;
142
+ saveUpdateState(state, stateFilePath);
143
+ }
144
+ /**
145
+ * Record an update check: set lastCheckAt and lastCheckVersion, then persist.
146
+ *
147
+ * @param latestVersion - The latest version found during the check
148
+ * @param stateFilePath - Override path for testing
149
+ */
150
+ export function recordCheck(latestVersion, stateFilePath) {
151
+ const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
152
+ state.lastCheckAt = new Date().toISOString();
153
+ state.lastCheckVersion = latestVersion;
154
+ saveUpdateState(state, stateFilePath);
155
+ }
156
+ //# sourceMappingURL=updateState.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Proxy Quiet Detector
3
+ * Determines whether the proxy has been idle (no traffic) for a given
4
+ * threshold by efficiently reading only the tail of today's debug log file.
5
+ * Used by the auto-update system to find safe windows for restarts.
6
+ */
7
+ /** Result of a traffic-quiet check. */
8
+ export interface QuietStatus {
9
+ isQuiet: boolean;
10
+ lastActivityAt: Date | null;
11
+ silenceDurationMs: number;
12
+ }
13
+ /**
14
+ * Check whether proxy traffic has been quiet (no requests) for at least
15
+ * `quietThresholdMs` milliseconds.
16
+ *
17
+ * Reads only the tail of today's debug log file for efficiency.
18
+ *
19
+ * @param quietThresholdMs Silence duration (ms) to consider "quiet". Default: 120 000 (2 min).
20
+ * @returns QuietStatus with the idle analysis.
21
+ */
22
+ export declare function checkTrafficQuiet(quietThresholdMs?: number): QuietStatus;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Proxy Quiet Detector
3
+ * Determines whether the proxy has been idle (no traffic) for a given
4
+ * threshold by efficiently reading only the tail of today's debug log file.
5
+ * Used by the auto-update system to find safe windows for restarts.
6
+ */
7
+ import { openSync, readSync, closeSync, fstatSync, existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { homedir } from "node:os";
10
+ /** Default quiet threshold: 2 minutes of no traffic. */
11
+ const DEFAULT_QUIET_THRESHOLD_MS = 120_000;
12
+ /** Maximum bytes to read from the tail of the log file. */
13
+ const TAIL_READ_SIZE = 4096;
14
+ /**
15
+ * Build the path to a proxy debug log file for a given date.
16
+ * Format: ~/.neurolink/logs/proxy-debug-YYYY-MM-DD.jsonl
17
+ */
18
+ function getLogPathForDate(date) {
19
+ const dateStr = date.toISOString().split("T")[0];
20
+ return join(homedir(), ".neurolink", "logs", `proxy-debug-${dateStr}.jsonl`);
21
+ }
22
+ /**
23
+ * Get the most relevant log file path. Uses today's log if it exists,
24
+ * otherwise falls back to yesterday's to handle the midnight rollover case
25
+ * (last request at 23:59, update check at 00:01).
26
+ */
27
+ function getActiveLogPath() {
28
+ const todayPath = getLogPathForDate(new Date());
29
+ if (existsSync(todayPath)) {
30
+ return todayPath;
31
+ }
32
+ const yesterday = new Date(Date.now() - 86_400_000);
33
+ return getLogPathForDate(yesterday);
34
+ }
35
+ /**
36
+ * Read the last complete line(s) from a file efficiently.
37
+ * Uses low-level fs to seek to end and read only the last TAIL_READ_SIZE bytes.
38
+ * Returns an array of the last non-empty lines (up to 2 for fallback).
39
+ */
40
+ function readTailLines(filePath) {
41
+ let fd = null;
42
+ try {
43
+ fd = openSync(filePath, "r");
44
+ const stat = fstatSync(fd);
45
+ if (stat.size === 0) {
46
+ return [];
47
+ }
48
+ const readSize = Math.min(TAIL_READ_SIZE, stat.size);
49
+ const offset = stat.size - readSize;
50
+ const buffer = Buffer.alloc(readSize);
51
+ readSync(fd, buffer, 0, readSize, offset);
52
+ const chunk = buffer.toString("utf-8");
53
+ // Split into lines, filter out empty trailing entries
54
+ const lines = chunk.split("\n").filter((line) => line.trim().length > 0);
55
+ // Return last 2 lines (last + fallback)
56
+ return lines.slice(-2);
57
+ }
58
+ finally {
59
+ if (fd !== null) {
60
+ closeSync(fd);
61
+ }
62
+ }
63
+ }
64
+ /**
65
+ * Try to parse a JSON line and extract its ISO timestamp.
66
+ * Returns the timestamp as epoch ms, or null if parsing fails.
67
+ */
68
+ function extractTimestamp(line) {
69
+ try {
70
+ const parsed = JSON.parse(line);
71
+ if (typeof parsed.timestamp === "string") {
72
+ const ms = Date.parse(parsed.timestamp);
73
+ if (!Number.isNaN(ms)) {
74
+ return ms;
75
+ }
76
+ }
77
+ }
78
+ catch {
79
+ // Malformed JSON — caller will handle fallback
80
+ }
81
+ return null;
82
+ }
83
+ /**
84
+ * Check whether proxy traffic has been quiet (no requests) for at least
85
+ * `quietThresholdMs` milliseconds.
86
+ *
87
+ * Reads only the tail of today's debug log file for efficiency.
88
+ *
89
+ * @param quietThresholdMs Silence duration (ms) to consider "quiet". Default: 120 000 (2 min).
90
+ * @returns QuietStatus with the idle analysis.
91
+ */
92
+ export function checkTrafficQuiet(quietThresholdMs = DEFAULT_QUIET_THRESHOLD_MS) {
93
+ const noActivityResult = {
94
+ isQuiet: true,
95
+ lastActivityAt: null,
96
+ silenceDurationMs: Infinity,
97
+ };
98
+ const logPath = getActiveLogPath();
99
+ if (!existsSync(logPath)) {
100
+ return noActivityResult;
101
+ }
102
+ const tailLines = readTailLines(logPath);
103
+ if (tailLines.length === 0) {
104
+ return noActivityResult;
105
+ }
106
+ // Try last line first, then fall back to the one before it
107
+ let timestampMs = null;
108
+ for (let i = tailLines.length - 1; i >= 0; i--) {
109
+ timestampMs = extractTimestamp(tailLines[i]);
110
+ if (timestampMs !== null) {
111
+ break;
112
+ }
113
+ }
114
+ if (timestampMs === null) {
115
+ // All tail lines are malformed — treat as quiet
116
+ return noActivityResult;
117
+ }
118
+ const silenceDurationMs = Date.now() - timestampMs;
119
+ return {
120
+ isQuiet: silenceDurationMs >= quietThresholdMs,
121
+ lastActivityAt: new Date(timestampMs),
122
+ silenceDurationMs,
123
+ };
124
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Proxy auto-update version checker.
3
+ *
4
+ * Queries the npm registry for the latest published version of
5
+ * `@juspay/neurolink` and compares it against the currently running version.
6
+ * Designed to be non-blocking and failure-tolerant — any error (network,
7
+ * timeout, parse) silently returns `updateAvailable: false`.
8
+ */
9
+ export interface UpdateCheckResult {
10
+ currentVersion: string;
11
+ latestVersion: string;
12
+ updateAvailable: boolean;
13
+ }
14
+ /**
15
+ * Query npm for the latest version of `@juspay/neurolink` and compare it
16
+ * against {@link currentVersion}.
17
+ *
18
+ * On **any** failure the function resolves (never rejects) with
19
+ * `{ updateAvailable: false, latestVersion: currentVersion }`.
20
+ */
21
+ export declare function checkForUpdate(currentVersion: string): Promise<UpdateCheckResult>;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Proxy auto-update version checker.
3
+ *
4
+ * Queries the npm registry for the latest published version of
5
+ * `@juspay/neurolink` and compares it against the currently running version.
6
+ * Designed to be non-blocking and failure-tolerant — any error (network,
7
+ * timeout, parse) silently returns `updateAvailable: false`.
8
+ */
9
+ import { execFile as execFileCb } from "node:child_process";
10
+ import { promisify } from "node:util";
11
+ import { logger } from "../utils/logger.js";
12
+ const execFile = promisify(execFileCb);
13
+ /** Timeout (ms) for the `npm view` child process. */
14
+ const NPM_VIEW_TIMEOUT_MS = 10_000;
15
+ /**
16
+ * Parse a version string of the form `major.minor.patch` into numeric
17
+ * components. Returns `null` when the string does not match.
18
+ */
19
+ function parseSemVer(version) {
20
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
21
+ if (!match) {
22
+ return null;
23
+ }
24
+ return {
25
+ major: Number(match[1]),
26
+ minor: Number(match[2]),
27
+ patch: Number(match[3]),
28
+ };
29
+ }
30
+ /**
31
+ * Returns `true` when `latest` is strictly greater than `current`.
32
+ *
33
+ * Both arguments must be valid semver strings; returns `false` on any
34
+ * parse failure so the caller never sees a spurious "update available".
35
+ */
36
+ function isNewerVersion(current, latest) {
37
+ const cur = parseSemVer(current);
38
+ const lat = parseSemVer(latest);
39
+ if (!cur || !lat) {
40
+ return false;
41
+ }
42
+ if (lat.major !== cur.major) {
43
+ return lat.major > cur.major;
44
+ }
45
+ if (lat.minor !== cur.minor) {
46
+ return lat.minor > cur.minor;
47
+ }
48
+ return lat.patch > cur.patch;
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // Core check
52
+ // ---------------------------------------------------------------------------
53
+ /**
54
+ * Query npm for the latest version of `@juspay/neurolink` and compare it
55
+ * against {@link currentVersion}.
56
+ *
57
+ * On **any** failure the function resolves (never rejects) with
58
+ * `{ updateAvailable: false, latestVersion: currentVersion }`.
59
+ */
60
+ export async function checkForUpdate(currentVersion) {
61
+ const fail = {
62
+ currentVersion,
63
+ latestVersion: currentVersion,
64
+ updateAvailable: false,
65
+ };
66
+ try {
67
+ logger.debug("[UpdateChecker] Checking for updates", { currentVersion });
68
+ const { stdout } = await execFile("npm", ["view", "@juspay/neurolink", "version", "--json"], { timeout: NPM_VIEW_TIMEOUT_MS });
69
+ // `npm view ... --json` wraps the value in double-quotes, e.g. `"9.32.0"`
70
+ const parsed = JSON.parse(stdout);
71
+ if (typeof parsed !== "string") {
72
+ logger.warn("[UpdateChecker] Unexpected npm output type", {
73
+ type: typeof parsed,
74
+ });
75
+ return fail;
76
+ }
77
+ const latestVersion = parsed.trim();
78
+ if (!parseSemVer(latestVersion)) {
79
+ logger.warn("[UpdateChecker] Failed to parse latest version", {
80
+ latestVersion,
81
+ });
82
+ return fail;
83
+ }
84
+ const updateAvailable = isNewerVersion(currentVersion, latestVersion);
85
+ logger.debug("[UpdateChecker] Version check complete", {
86
+ currentVersion,
87
+ latestVersion,
88
+ updateAvailable,
89
+ });
90
+ return { currentVersion, latestVersion, updateAvailable };
91
+ }
92
+ catch (error) {
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ logger.warn("[UpdateChecker] Update check failed", { error: message });
95
+ return fail;
96
+ }
97
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Update State Persistence
3
+ * Manages persistent state for the proxy auto-update feature.
4
+ * Tracks check timestamps, suppressed versions, and update history.
5
+ *
6
+ * State file location: ~/.neurolink/update-state.json
7
+ * Suppressed versions expire after 24 hours.
8
+ */
9
+ export interface SuppressedVersion {
10
+ suppressedAt: string;
11
+ reason: string;
12
+ }
13
+ export interface UpdateState {
14
+ lastCheckAt: string;
15
+ lastCheckVersion: string;
16
+ suppressedVersions: Record<string, SuppressedVersion>;
17
+ lastUpdateAt: string | null;
18
+ lastUpdateVersion: string | null;
19
+ }
20
+ /**
21
+ * Return an empty/initial UpdateState.
22
+ */
23
+ export declare function getDefaultUpdateState(): UpdateState;
24
+ /**
25
+ * Load the update state from disk.
26
+ * Returns null if the file does not exist.
27
+ * Returns the default state if the file contains corrupt JSON.
28
+ *
29
+ * @param stateFilePath - Override path for testing (default: ~/.neurolink/update-state.json)
30
+ */
31
+ export declare function loadUpdateState(stateFilePath?: string): UpdateState | null;
32
+ /**
33
+ * Save the update state to disk.
34
+ *
35
+ * @param state - The UpdateState to persist
36
+ * @param stateFilePath - Override path for testing (default: ~/.neurolink/update-state.json)
37
+ */
38
+ export declare function saveUpdateState(state: UpdateState, stateFilePath?: string): void;
39
+ /**
40
+ * Check whether a version is currently suppressed (i.e., suppressed AND within the 24-hour window).
41
+ *
42
+ * @param version - Semver version string to check
43
+ * @param stateFilePath - Override path for testing
44
+ */
45
+ export declare function isVersionSuppressed(version: string, stateFilePath?: string): boolean;
46
+ /**
47
+ * Add a version to the suppressed list and persist.
48
+ *
49
+ * @param version - Semver version string to suppress
50
+ * @param reason - Human-readable reason for suppression
51
+ * @param stateFilePath - Override path for testing
52
+ */
53
+ export declare function suppressVersion(version: string, reason: string, stateFilePath?: string): void;
54
+ /**
55
+ * Record a successful update: set lastUpdateAt and lastUpdateVersion, then persist.
56
+ *
57
+ * @param version - The version that was successfully installed
58
+ * @param stateFilePath - Override path for testing
59
+ */
60
+ export declare function recordSuccessfulUpdate(version: string, stateFilePath?: string): void;
61
+ /**
62
+ * Record an update check: set lastCheckAt and lastCheckVersion, then persist.
63
+ *
64
+ * @param latestVersion - The latest version found during the check
65
+ * @param stateFilePath - Override path for testing
66
+ */
67
+ export declare function recordCheck(latestVersion: string, stateFilePath?: string): void;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Update State Persistence
3
+ * Manages persistent state for the proxy auto-update feature.
4
+ * Tracks check timestamps, suppressed versions, and update history.
5
+ *
6
+ * State file location: ~/.neurolink/update-state.json
7
+ * Suppressed versions expire after 24 hours.
8
+ */
9
+ import fs from "fs";
10
+ import os from "os";
11
+ import path from "path";
12
+ // ============================================
13
+ // Constants
14
+ // ============================================
15
+ const STATE_FILENAME = "update-state.json";
16
+ const SUPPRESSION_TTL_MS = 86_400_000; // 24 hours
17
+ // ============================================
18
+ // Internal Helpers
19
+ // ============================================
20
+ /**
21
+ * Resolve the path to the update state file.
22
+ * Accepts an override for testing; defaults to ~/.neurolink/update-state.json.
23
+ */
24
+ function resolveStatePath(overridePath) {
25
+ if (overridePath) {
26
+ return overridePath;
27
+ }
28
+ return path.join(os.homedir(), ".neurolink", STATE_FILENAME);
29
+ }
30
+ /**
31
+ * Ensure the parent directory of the given file path exists.
32
+ */
33
+ function ensureParentDir(filePath) {
34
+ const dir = path.dirname(filePath);
35
+ if (!fs.existsSync(dir)) {
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ }
38
+ }
39
+ // ============================================
40
+ // Exported Functions
41
+ // ============================================
42
+ /**
43
+ * Return an empty/initial UpdateState.
44
+ */
45
+ export function getDefaultUpdateState() {
46
+ return {
47
+ lastCheckAt: new Date(0).toISOString(),
48
+ lastCheckVersion: "",
49
+ suppressedVersions: {},
50
+ lastUpdateAt: null,
51
+ lastUpdateVersion: null,
52
+ };
53
+ }
54
+ /**
55
+ * Load the update state from disk.
56
+ * Returns null if the file does not exist.
57
+ * Returns the default state if the file contains corrupt JSON.
58
+ *
59
+ * @param stateFilePath - Override path for testing (default: ~/.neurolink/update-state.json)
60
+ */
61
+ export function loadUpdateState(stateFilePath) {
62
+ const filePath = resolveStatePath(stateFilePath);
63
+ try {
64
+ if (!fs.existsSync(filePath)) {
65
+ return null;
66
+ }
67
+ const content = fs.readFileSync(filePath, "utf8");
68
+ const parsed = JSON.parse(content);
69
+ // Minimal shape check — reject valid JSON that isn't an UpdateState
70
+ // Note: typeof null === "object", so we check both
71
+ if (typeof parsed !== "object" ||
72
+ parsed === null ||
73
+ typeof parsed.suppressedVersions !== "object" ||
74
+ parsed.suppressedVersions === null ||
75
+ Array.isArray(parsed.suppressedVersions) ||
76
+ typeof parsed.lastCheckAt !== "string") {
77
+ return getDefaultUpdateState();
78
+ }
79
+ return parsed;
80
+ }
81
+ catch {
82
+ // Corrupt or unreadable JSON — return default state
83
+ return getDefaultUpdateState();
84
+ }
85
+ }
86
+ /**
87
+ * Save the update state to disk.
88
+ *
89
+ * @param state - The UpdateState to persist
90
+ * @param stateFilePath - Override path for testing (default: ~/.neurolink/update-state.json)
91
+ */
92
+ export function saveUpdateState(state, stateFilePath) {
93
+ const filePath = resolveStatePath(stateFilePath);
94
+ ensureParentDir(filePath);
95
+ // Atomic write: write to temp file then rename to prevent corruption on crash
96
+ const tmpPath = filePath + ".tmp";
97
+ fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
98
+ fs.renameSync(tmpPath, filePath);
99
+ }
100
+ /**
101
+ * Check whether a version is currently suppressed (i.e., suppressed AND within the 24-hour window).
102
+ *
103
+ * @param version - Semver version string to check
104
+ * @param stateFilePath - Override path for testing
105
+ */
106
+ export function isVersionSuppressed(version, stateFilePath) {
107
+ const state = loadUpdateState(stateFilePath);
108
+ if (!state) {
109
+ return false;
110
+ }
111
+ const entry = state.suppressedVersions[version];
112
+ if (!entry) {
113
+ return false;
114
+ }
115
+ return Date.now() - Date.parse(entry.suppressedAt) < SUPPRESSION_TTL_MS;
116
+ }
117
+ /**
118
+ * Add a version to the suppressed list and persist.
119
+ *
120
+ * @param version - Semver version string to suppress
121
+ * @param reason - Human-readable reason for suppression
122
+ * @param stateFilePath - Override path for testing
123
+ */
124
+ export function suppressVersion(version, reason, stateFilePath) {
125
+ const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
126
+ state.suppressedVersions[version] = {
127
+ suppressedAt: new Date().toISOString(),
128
+ reason,
129
+ };
130
+ saveUpdateState(state, stateFilePath);
131
+ }
132
+ /**
133
+ * Record a successful update: set lastUpdateAt and lastUpdateVersion, then persist.
134
+ *
135
+ * @param version - The version that was successfully installed
136
+ * @param stateFilePath - Override path for testing
137
+ */
138
+ export function recordSuccessfulUpdate(version, stateFilePath) {
139
+ const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
140
+ state.lastUpdateAt = new Date().toISOString();
141
+ state.lastUpdateVersion = version;
142
+ saveUpdateState(state, stateFilePath);
143
+ }
144
+ /**
145
+ * Record an update check: set lastCheckAt and lastCheckVersion, then persist.
146
+ *
147
+ * @param latestVersion - The latest version found during the check
148
+ * @param stateFilePath - Override path for testing
149
+ */
150
+ export function recordCheck(latestVersion, stateFilePath) {
151
+ const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
152
+ state.lastCheckAt = new Date().toISOString();
153
+ state.lastCheckVersion = latestVersion;
154
+ saveUpdateState(state, stateFilePath);
155
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.34.0",
3
+ "version": "9.36.0",
4
4
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
5
5
  "author": {
6
6
  "name": "Juspay Technologies",