UNPKG

@essentialai/cogent-bridge

Version:

Cogent Bridge — cross-agent comms for Claude Code, OpenAI Codex and Slack. Codex users: install with `curl -fsSL https://cogent.tools/install.sh | sh`, then start with `cogent-codex` for real-time peer wake.

136 lines • 6.01 kB
import { logger } from "../logger.js"; /** * Best-effort proactive update check for the cogent-bridge npm client. * * On startup the MCP server knows its own SERVER_VERSION but not whether a newer * version has been published — so users stay on stale clients/plugins indefinitely * (the recurring "/cogent map still shows the old form" pain). This module fetches * the latest published version and, if the running build is behind, produces a * one-line nudge the caller prints to STDERR (never stdout — that's the MCP protocol). * * Discipline (mirrors relay-version-cache.ts): never throws, returns null on any * failure / when current, and is bounded by a short timeout so it never delays the * server becoming ready. The caller fires it and forgets it. */ const NPM_LATEST_URL = "https://registry.npmjs.org/@essentialai/cogent-bridge/latest"; /** Parse a leading `x.y.z` into numbers; null if not a clean 3-part numeric version. */ export function parseVersion(v) { const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim()); if (!m) return null; return [Number(m[1]), Number(m[2]), Number(m[3])]; } /** True iff `latest` is strictly newer than `current` (major, then minor, then patch). */ export function isNewer(current, latest) { const c = parseVersion(current); const l = parseVersion(latest); if (!c || !l) return false; for (let i = 0; i < 3; i++) { if (l[i] > c[i]) return true; if (l[i] < c[i]) return false; } return false; } /** * Process-lifetime store for the startup update check's result, so other surfaces * (e.g. cogent_health_check) can show the same nudge without a second npm fetch. The * MCP server is long-lived and checks once at boot; null means "current or not yet/failed". */ let lastNudge = null; /** Nudge text already handed to a surfacing path, so it is announced ONCE, not on repeat. */ let shownNudge = null; /** * Record a check's result (the nudge string, or null when current/unchecked). * * A CHANGED nudge re-arms {@link consumeNudgeOnce}: a second release must still be announced even * though the first one already was. Re-setting the SAME text does not re-arm — that is what keeps * a periodic check from becoming a periodic nag. */ export function setLastNudge(nudge) { if (nudge !== lastNudge) shownNudge = null; lastNudge = nudge; } /** * The pending nudge, returned EXACTLY ONCE per distinct nudge; null afterwards. * * 🔴 WHY "ONCE" IS PART OF THE CONTRACT. The surfacing path rides a message the agent is already * processing (auto-relay), so it costs no extra turn — but that also means it would otherwise fire * on EVERY inbound message. One line per release is information; the same line on every message is * noise, and noise is what teaches people to ignore the notice that matters. */ export function consumeNudgeOnce() { if (!lastNudge || lastNudge === shownNudge) return null; shownNudge = lastNudge; return lastNudge; } /** * Re-check for a newer published client on an interval, forever. * * 🔴 WHY A ONE-SHOT CHECK WAS NEARLY USELESS. The check ran once, at boot. Every bridge here is * long-lived — some run for weeks — so a version published after startup was invisible to the * running process no matter how many releases shipped. 3.21.4 through 3.21.8 all went out inside a * single day; an agent booted that morning would still have reported itself current that evening. * * Checks immediately, then every `intervalMs`. Never throws (checkForUpdate already swallows), and * the timer is `unref`'d so it can never hold the process open at shutdown. Returns a stop * function — used by tests, and available to any caller that needs a clean teardown. */ export function startUpdateCheckLoop(current, opts = {}) { const { intervalMs = 6 * 60 * 60 * 1000, ...checkOpts } = opts; const run = () => { void checkForUpdate(current, checkOpts).then((nudge) => { // Only ADOPT a positive result. A failed/timed-out check must not erase a nudge we already // hold — "we could not reach npm" is not evidence that the client is current. if (nudge) setLastNudge(nudge); }); }; run(); const timer = setInterval(run, intervalMs); timer.unref?.(); return () => clearInterval(timer); } /** The most recent update nudge, or null. Read by health-check to surface it to the user. */ export function getLastNudge() { return lastNudge; } /** Test-only: clear the stored nudge between suites. @internal */ export function __resetLastNudgeForTest() { lastNudge = null; shownNudge = null; } /** * Fetch the latest published cogent-bridge version; if newer than `current`, return a * one-line update nudge, else null. Never throws. */ export async function checkForUpdate(current, opts = {}) { const { timeoutMs = 2500, url = NPM_LATEST_URL, fetchImpl = fetch } = opts; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const resp = await fetchImpl(url, { signal: controller.signal }); if (!resp.ok) return null; const body = (await resp.json()); const latest = body?.version; if (typeof latest !== "string" || !isNewer(current, latest)) return null; return (`⚠ Cogent update available: v${current} → v${latest}. ` + `Update the client (clear ~/.npm/_npx then restart your agent) AND the plugin ` + `(\`/plugin update cogent@cogent\`, then restart Claude Code) — Team \`/cogent map\` ` + `needs the latest. https://github.com/eaisdevelopment/cogent`); } catch (err) { logger.debug(`npm-update-check: ${err instanceof Error ? err.message : String(err)}`); return null; } finally { clearTimeout(timer); } } //# sourceMappingURL=npm-update-check.js.map