UNPKG

run402-mcp

Version:

MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.

110 lines • 4.51 kB
/** * The per-directory binding file — `.run402.json`, and its gitignored personal * override `.run402.local.json`. * * A checkout binds itself to a wallet profile, an organization, a room, by * committing a small JSON file. That is a CHECKOUT-LEVEL CONTRACT, not a CLI * implementation detail: every agent working in the directory inherits the * same one, whichever surface it reaches Run402 through. * * It lives in core because two surfaces now read it. The CLI has always walked * this chain; the MCP server needs the same answer for the same directory, and * `run402-mcp` ships only `dist` + `core/dist` + `sdk/dist` — `cli/` is not in * the published package, so the MCP server cannot import the CLI's copy even * if it wanted to. One reader, in the one place both can see it, rather than * two that agree until they don't. * * Deliberately pure: `node:fs` + `node:path`, no logging, no process exit, no * validation of what a value MEANS. Callers decide whether a value is * well-shaped and what to do when it isn't — the CLI exits, the MCP server * returns an error, and neither behaviour belongs in a file reader. * * Unknown keys are ignored on purpose: that is what makes an older client * forward-compatible with a binding file written by a newer one. */ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; /** The committed binding file. Safe to check in — it names, never authorizes. */ export const BINDING_FILE = ".run402.json"; /** The personal override. Gitignored; beats the committed file key-by-key. */ export const BINDING_LOCAL_FILE = ".run402.local.json"; /** Read one key from a single directory's binding files, override first. */ function readBindingKeyFrom(dir, key) { for (const fname of [BINDING_LOCAL_FILE, BINDING_FILE]) { const p = join(dir, fname); try { const parsed = JSON.parse(readFileSync(p, "utf8")); const v = parsed?.[key]; if (typeof v === "string" && v.trim()) return { value: v.trim(), file: p }; } catch { /* missing / unreadable / malformed → skip */ } } return null; } /** * Nearest binding of `key`, walking up from `startDir` to the filesystem root. * * Resolution is PER KEY, not per file: the nearest file that carries the key * wins, so `/work/.run402.json` may supply the organization while * `/work/api/.run402.json` supplies the wallet. A worktree nested under a bound * checkout therefore inherits the binding without restating it. */ export function findBindingKey(startDir, key) { let dir = resolve(startDir); for (;;) { const hit = readBindingKeyFrom(dir, key); if (hit) return hit; const parent = dirname(dir); if (parent === dir) return null; dir = parent; } } /** Path of the committed binding file for a directory. */ export function bindingFilePath(dir = process.cwd()) { return join(dir, BINDING_FILE); } /** Parse a directory's committed binding file, or `{}` when absent/unreadable. */ export function readBindingFile(dir = process.cwd()) { try { const parsed = JSON.parse(readFileSync(bindingFilePath(dir), "utf8")); return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; } catch { return {}; } } /** * MERGE keys into a directory's committed binding file. A `null` (or * `undefined`) value removes its key; a file left with no keys is deleted * rather than committed empty. * * The file is shared by tiers (`wallet` from `wallets bind`, `org`/`room` from * `orgs bind`) and unknown keys are preserved, so one tier can never clobber * another's binding. */ export function updateBindingFile(dir, patch) { const file = bindingFilePath(dir); const next = { ...readBindingFile(dir) }; for (const [k, v] of Object.entries(patch)) { if (v === null || v === undefined) delete next[k]; else next[k] = v; } if (Object.keys(next).length === 0) { const existed = existsSync(file); if (existed) rmSync(file, { force: true }); return { file, contents: null, removed: existed }; } writeFileSync(file, JSON.stringify(next, null, 2) + "\n"); return { file, contents: next, removed: false }; } //# sourceMappingURL=binding-file.js.map