export interface CliRelease { version: string; sha256: string; } export interface Caps { edition?: string; cli?: CliRelease | null; } /** semver-ish compare: -1 (ab). Ignores prerelease tags. */ export declare function compareVersions(a: string, b: string): number; /** * Enabled = explicit flag wins; else default ON for EVERY edition. * * Cloud used to default OFF, which — with nothing at install/init ever turning * it on — meant SaaS customers silently ran a stale, buggy collector forever * and never received fixes. That's a dead end, not a security posture. The * update is same-origin only and sha256-pinned to what the server advertises, * so default-on just means "trust the server you're already syncing to." Set * CHAT_RECALL_AUTO_UPDATE=0 to opt out. */ export declare function isAutoUpdateEnabled(_edition: string | undefined, flag: string | undefined): boolean; /** * Where the CLI tarball is served. With a version, the URL names it: a server * answers it only while that is the version it holds. The unversioned URL is * served by whichever pod answers, so during a rollout a client told 0.7.3 by * a new pod downloaded 0.7.2 from an old one and failed its checksum. */ export declare const tarballUrl: (base: string, version?: string) => string; /** The higher of two versions, treating an unparseable one as older. */ export declare function newerOf(a: string, b: string | null): string; /** * The version currently ON DISK, read fresh every call. * * Deliberately not cached and deliberately not the module-load constant: the * whole point is to notice that an update already landed underneath a * long-running process. Returns null when the manifest cannot be read, which * leaves the caller's own value in charge rather than inventing one. */ export declare function installedVersion(): string | null; export interface UpdatePlan { update: boolean; reason: string; url?: string; /** The unversioned URL, for a server that predates the versioned one. */ fallbackUrl?: string; sha256?: string; from?: string; to?: string; } /** Pure: decide whether to update, and to what — no I/O. */ export declare function planAutoUpdate(base: string, caps: Caps, ownVersion: string, flag: string | undefined): UpdatePlan; /** * Remove staging dirs left behind by older versions. * * The cleanup below only helps someone who installs a build that HAS it. Every * machine that ran an earlier version is already carrying the wreckage — the * measured case was 5,889 directories and 5.1 GB, and fixing the leak does not * give that disk back. So the updater also sweeps on the way past. * * ONLY OUR OWN, and only once they are cold: * - the name must be `cr-update-` + exactly six characters, mkdtemp's shape on * every platform, so a directory called `cr-update-notes` is never touched; * - it must hold nothing but `chat-recall.tgz`, which is the only file this * code ever writes there — anything else means it is not ours; * - it must be older than an hour, so a concurrent updater's live staging dir * is never pulled out from under it. * * Defaults to `tmpdir()`, the same root mkdtemp writes to, so it follows TMPDIR * on macOS (`/var/folders/…/T`) and %TEMP% on Windows rather than assuming * `/tmp`. macOS purges that directory itself eventually; Linux mostly does not, * which is where the backlog was measured. * * Best-effort by design: a sweep that throws must not fail an update. */ export declare function sweepStaleStaging(root?: string, now?: number): number; export interface UpdateDeps { download: (url: string) => Promise; install: (tgzPath: string) => void; restart: (platform: NodeJS.Platform) => void; platform?: NodeJS.Platform; /** Version actually on disk after the install — the post-condition check. */ verify?: () => string | null; /** True once this process is shutting down: nothing is installed or restarted after that. */ stopping?: () => boolean; /** Version on disk before the install, read once the lock is held. */ onDisk?: () => string | null; /** Take the machine-wide update lock, or null when another process holds it. */ acquireLock?: () => { release(): void; } | null; } export interface UpdateResult { updated: boolean; reason: string; from?: string; to?: string; } /** Execute a plan: download → checksum-verify → install → restart. */ export declare function executeAutoUpdate(plan: UpdatePlan, deps: UpdateDeps): Promise; /** * The npm prefix the RUNNING copy was installed into. * * `/lib/node_modules/chat-recall/dist/auto-update.js` → ``. * Returns null when this file is not inside a node_modules tree (a source * checkout, a bundled binary), where there is nothing to reinstall over. */ export declare function runningPrefix(moduleUrl?: string): string | null; export interface NpmCommand { /** Executable to spawn. */ file: string; /** Arguments that come before npm's own (the npm-cli.js path when `file` is node). */ args: string[]; /** Environment for the child, with the running node's directory first on PATH. */ env: NodeJS.ProcessEnv; /** Run through the shell: a bare `npm` resolved from PATH (npm.cmd on Windows). */ shell: boolean; } export interface NpmLocation { execPath: string; prefix: string | null; platform: NodeJS.Platform; env: NodeJS.ProcessEnv; exists: (p: string) => boolean; } /** * The npm that belongs to the node running this process. * * A macOS collector ran `npm install -g --prefix "/opt/homebrew" …` and failed * 5,769 times from 2026-09-02 to 2026-09-24. launchd starts an agent with * PATH=/usr/bin:/bin:/usr/sbin:/sbin and the plist sets no PATH, so `/bin/sh` * found no `npm`: Homebrew installs it in /opt/homebrew/bin. The same install * from a terminal worked: that device went 0.5.30 → 0.5.31 → 0.5.32 in 82 * seconds on 2026-09-01, by hand, after 318 failed attempts by the daemon. * * So npm is found from process.execPath and the prefix this copy runs from, and * it runs as `node npm-cli.js`. The npm launcher script starts with * `#!/usr/bin/env node`, which fails the same way under the launchd PATH. The * running node's directory also goes first on the child's PATH, for anything * npm spawns. A bare `npm` from PATH is the last candidate. */ export declare function resolveNpm(loc: NpmLocation): NpmCommand; /** * The command that restarts the watch service, per platform. * * Linux passes --no-block: queue the restart and return. The watch daemon runs * this from inside the service, and a blocking `systemctl restart` waits for * the stop phase, which waits for this process, whose event loop the wait * blocks. systemd then killed it at TimeoutStopSec and the next start counted * a crash. */ export declare function restartCommand(platform: NodeJS.Platform): { cmd: string; shell?: string; } | null; type UpdateState = { target?: string; failures?: number; }; export declare function readUpdateState(): UpdateState; /** True when this device has already given up on installing `target`. */ export declare function hasGivenUpOn(target: string, st?: UpdateState): boolean; /** Count a failed attempt; returns the new consecutive count for `target`. */ export declare function noteUpdateFailure(target: string): number; /** A successful install clears the brake. */ export declare function clearUpdateFailures(): void; /** The reason an update gives when its process began to shut down under it. */ export declare const STOPPING_REASON = "not installed: this process is shutting down"; /** The reason an update gives when another process holds the update lock. */ export declare const LOCKED_REASON = "another process is installing an update"; /** * Stop this process from installing or restarting anything from now on. The * watch daemon calls it when it begins to shut down: an update that started * then ran `systemctl restart` against the unit that was stopping it. */ export declare function stopAutoUpdates(): void; export declare function runAutoUpdate(base: string, authHeaders: Record, ownVersion: string, deps?: Partial): Promise; export {}; //# sourceMappingURL=auto-update.d.ts.map