@martintrojer/murmur 0.1.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/ARCHITECTURE.md +357 -0
- package/README.md +117 -0
- package/dist/cli.js +1426 -0
- package/dist/cli.js.map +1 -0
- package/dist/extension/murmur-pi.js +224 -0
- package/dist/extension/murmur-pi.js.map +1 -0
- package/dist/extension/store.js +257 -0
- package/dist/extension/store.js.map +1 -0
- package/dist/index.d.ts +216 -0
- package/dist/index.js +894 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/agents.ts","../src/identity.ts","../src/paths.ts","../src/mux.ts","../src/channel.ts","../src/types.ts","../src/fold.ts","../src/export.ts","../src/collector.ts","../src/glance.ts","../src/status.ts","../src/store.ts","../src/index.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { loadIdentity } from \"./identity.js\";\nimport { tmux } from \"./mux.js\";\nimport type { Status } from \"./status.js\";\nimport type { Store } from \"./store.js\";\n\nexport type Agent = Status[\"agents\"][number];\n\n/**\n * The most specific human-readable name an agent has, never a tmux id.\n *\n * Four sources, most to least specific: mu's agent name, pi's session name,\n * the tmux window name, the tmux session name. The old picker showed window\n * names and that was the thing it did better than raw `$26:@79`; these are all\n * recorded on the event, so this reads the same for a local and a remote agent.\n *\n * Falls back to the window id only when a node recorded no names at all, which\n * means a pre-names event or a non-tmux harness.\n */\nexport function agentLabel(agent: Agent): string {\n const name = agent.agent_name ?? agent.pi_session ?? agent.window_name ?? agent.session_name;\n return terminalText(name ?? agent.window);\n}\n\n/**\n * Where the agent lives, for the second column. Names only -- the ids are what\n * jumps, not what a human reads.\n */\nexport function agentLocation(agent: Agent): string {\n const session = agent.session_name ?? agent.session;\n const window = agent.window_name ?? agent.window;\n return terminalText(session === window ? session : `${session}:${window}`);\n}\n\nexport function terminalText(value: string): string {\n return [...value]\n .map((character) => {\n const code = character.charCodeAt(0);\n return code < 0x20 || code === 0x7f || (code >= 0x80 && code <= 0x9f) ? \"�\" : character;\n })\n .join(\"\");\n}\n\nexport function shellQuote(value: string): string {\n return `'${value.replaceAll(\"'\", `'\\\\''`)}'`;\n}\n\nexport type JumpResult =\n | { ok: true }\n | { ok: false; reason: \"no_peer\" | \"unreachable\" | \"no_tmux\" | \"window_gone\"; message: string };\n\n/**\n * Drop a dead agent's rows from the local replica.\n *\n * Export on the authoring node clears dead windows, but that only runs when the\n * peer is next polled, and a window can die between a fetch and a jump. When a\n * jump proves the window is gone, the agent should leave this HUD now rather\n * than at the next collect.\n *\n * DELETE rather than append a `cleared` event, because this node cannot author\n * an event about another node's agent. `store.append` stamps the local host_id,\n * and `status()` folds local and remote events separately (local needs a pid\n * check, remote cannot have one) -- so a local row about a remote agent lands\n * in the other fold and shows up as a SECOND agent with the same agent_id,\n * which is exactly what it did before this was a delete.\n *\n * Deleting a replica is safe ONLY IF the rows can come back, and that needs the\n * peer's watermark rewound as well. Ingest asks for events after the watermark,\n * so deleting rows below it deletes them permanently: bubba's agents vanished\n * from the picker and no amount of collecting brought them back, even with the\n * node alive and the events still in its log.\n *\n * Rewinding to zero rather than to the deleted seq: the log is bounded by the\n * retention horizon, ingest is idempotent on (host_id, seq), and a re-read of a\n * small table is cheaper than tracking which seq belonged to which agent. The\n * next collect re-reads everything the peer still has, so if the window is\n * genuinely alive the agent reappears -- which is the answer to the race where\n * the host comes back up between the jump and the next poll.\n *\n * For a local agent there is no watermark and nothing to rewind: the pane is\n * gone, so nothing will ever author about it again.\n */\n/**\n * A jump proved this peer has no tmux server, so none of its agents exist.\n *\n * Drops every replicated row for that origin and rewinds the watermark, the\n * same recoverable delete `forgetReplica` does for one agent — just scoped to\n * the node, because \"no tmux server\" is a fact about the host rather than about\n * the window we happened to aim at. Leaving the rows and only labelling them\n * meant the picker kept offering four dead agents you had just been told were\n * gone.\n *\n * The mark stays on the peer as well: it is what stops an empty export being\n * read as recovery, and it is why the rows do not immediately reappear.\n */\nexport function forgetHostReplica(store: Store, hostId: string): void {\n try {\n const peer = store.peers().find((candidate) => candidate.host_id === hostId);\n store.forgetHost(hostId);\n if (peer) {\n // Watermark deliberately NOT rewound here, unlike the single-agent case.\n // Rewinding re-ingests the very rows just deleted, and because the\n // collector reads any ingest as \"the node is authoring again\", it also\n // cleared the mark -- so the dead agents reappeared looking healthy on\n // the next collect, one second later.\n //\n // Keeping the watermark means recovery waits for a NEW event, which is\n // the correct bar: the node has to actually say something before its\n // agents come back. Nothing is lost, since the rows describe windows a\n // live tmux server would re-announce.\n store.upsertPeer({\n name: peer.name,\n target: peer.target,\n tmux_down_at: Date.now(),\n });\n }\n } catch {\n // Advisory only: the next collect reconciles either way.\n }\n}\n\nexport function forgetReplica(store: Store, agentId: string, hostId: string): void {\n try {\n store.forgetAgent(agentId);\n const peer = store.peers().find((candidate) => candidate.host_id === hostId);\n if (peer) store.upsertPeer({ name: peer.name, target: peer.target, watermark: 0 });\n } catch {\n // Cosmetic only: the next collect reconciles either way.\n }\n}\n\nexport function jumpToAgent(store: Store, agent: Agent): JumpResult {\n const identity = loadIdentity();\n if (agent.host_id === identity?.host_id) {\n const live = tmux.liveWindows();\n if (live && !live.has(agent.window)) {\n forgetReplica(store, agent.agent_id, agent.host_id);\n return {\n ok: false,\n reason: \"window_gone\",\n message: `${agentLabel(agent)} is gone -- its window no longer exists. Cleared.`,\n };\n }\n tmux.attach(agent.session, agent.window);\n return { ok: true };\n }\n const peer = store.peers().find((candidate) => candidate.host_id === agent.host_id);\n const target = peer?.target ?? peer?.name;\n if (!target) {\n return {\n ok: false,\n reason: \"no_peer\",\n message: `No peer configured for host ${agent.host_id.slice(0, 8)}. Try: murmur peer add <target>`,\n };\n }\n\n // Check the window is still there before opening a window to attach to it.\n // Without this the attach fails inside a new tmux window that closes\n // instantly, which is indistinguishable from \"enter did nothing\" -- the\n // symptom that sent us looking for a quoting bug that did not exist.\n // ssh does not take an argv: it joins its arguments and hands the string to a\n // shell on the far side. An unquoted `#{window_id}` is mangled by that shell\n // and tmux answers `-F expects an argument`, which looked exactly like an\n // unreachable host. One quoted string, so the remote shell passes the format\n // through untouched.\n const probe = spawnSync(\n \"ssh\",\n [\"-o\", \"BatchMode=yes\", target, `tmux list-windows -a -F ${shellQuote(\"#{window_id}\")}`],\n { encoding: \"utf8\", timeout: 10_000 },\n );\n if (probe.status !== 0) {\n // 255 is ssh's own failure code; anything else came from the remote\n // command. Conflating them was wrong in the common case: with a warm\n // ControlMaster socket the host answers instantly and it is tmux that is\n // gone, so \"unreachable\" sent you looking at the network for a problem that\n // was not there.\n const sshFailed = probe.status === 255 || probe.error !== undefined;\n if (sshFailed) {\n // No mark: we learned nothing about the peer's tmux, only that we could\n // not ask. Its agents may be perfectly alive behind a cold socket or a\n // sleeping laptop, and deleting them here would be guessing.\n return {\n ok: false,\n reason: \"unreachable\",\n message: `cannot reach ${target} over ssh. The collector never prompts for auth, so connect once by hand to warm the connection, then retry.`,\n };\n }\n\n // ssh worked, tmux did not. That is a real fact about the host and the\n // strongest one available: a successful export only proves the murmur\n // binary ran, which it does happily on a box whose tmux server is gone --\n // which is why these agents read as fresh for three hours.\n forgetHostReplica(store, agent.host_id);\n return {\n ok: false,\n reason: \"no_tmux\",\n message: `${target} has no tmux server running, so its agents are gone. Removed them; they will come back when it reports again.`,\n };\n }\n const remoteWindows = new Set((probe.stdout ?? \"\").split(\"\\n\").filter(Boolean));\n if (!remoteWindows.has(agent.window)) {\n forgetReplica(store, agent.agent_id, agent.host_id);\n return {\n ok: false,\n reason: \"window_gone\",\n message: `${agentLabel(agent)} is gone -- ${target} no longer has that window. Cleared.`,\n };\n }\n\n const attachTarget = shellQuote(`${agent.session}:${agent.window}`);\n\n // Hand the ssh to tmux as its own window rather than running it here.\n // `murmur pick` is usually a display-popup, and a popup is modal: an ssh\n // session started inside it is killed the moment the picker exits, so the\n // remote pane flashed and vanished. A new window outlives the popup and\n // gives the remote tmux a real terminal to attach to.\n //\n // Nested tmux is the known cost here (see the spec's open question on inner\n // prefixes); a window at least makes it visible and closable.\n if (process.env.TMUX) {\n // `tmux new-window <command>` runs the command through a shell, so the\n // string is expanded LOCALLY before ssh sees it. A tmux session id is\n // always `$N`, so `$0:@6` arrived as `:@6` and the remote attach failed\n // with \"can't find session\". shellQuote alone is not enough: it protects\n // the remote shell, this protects the local one.\n const command = `ssh -t ${shellQuote(target)} tmux attach -t ${shellQuote(attachTarget)}`;\n const name = `@${peer?.display_name ?? target}`;\n\n // Reuse an existing window for this host rather than stacking a new one on\n // every jump. murmur navigates to agents; the window is only here because a\n // remote attach needs a terminal that outlives the popup, so one per host is\n // the whole requirement. Jumping to bubba three times used to leave three\n // identical @bubba windows behind.\n //\n // Matched on window name, which is the only handle available: the ssh is\n // opaque from here, and the remote session id is not a local address.\n const existing = tmux.windowNamed(name);\n if (existing) {\n tmux.selectWindow(existing);\n return { ok: true };\n }\n\n spawnSync(\"tmux\", [\"new-window\", \"-n\", name, command], { stdio: \"ignore\" });\n return { ok: true };\n }\n\n // Outside tmux there is no popup to escape, so run it directly.\n spawnSync(\"ssh\", [\"-t\", target, \"tmux\", \"attach\", \"-t\", attachTarget], { stdio: \"inherit\" });\n return { ok: true };\n}\n","import { randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { hostname } from \"node:os\";\nimport { join } from \"node:path\";\nimport { stateDir } from \"./paths.js\";\n\nexport type NodeIdentity = {\n host_id: string;\n display_name: string;\n};\n\nexport function loadIdentity(): NodeIdentity | null {\n const path = join(stateDir(), \"identity.json\");\n return existsSync(path) ? JSON.parse(readFileSync(path, \"utf8\")) : null;\n}\n\nexport function ensureIdentity(displayName = hostname()): NodeIdentity {\n const existing = loadIdentity();\n if (existing) return existing;\n\n const identity = { host_id: randomUUID(), display_name: displayName };\n mkdirSync(stateDir(), { recursive: true });\n writeFileSync(join(stateDir(), \"identity.json\"), `${JSON.stringify(identity, null, 2)}\\n`);\n return identity;\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function stateDir(): string {\n return (\n process.env.MURMUR_STATE_DIR ??\n join(process.env.XDG_STATE_HOME ?? join(homedir(), \".local\", \"state\"), \"murmur\")\n );\n}\n\nexport function configDir(): string {\n return (\n process.env.MURMUR_CONFIG_DIR ??\n join(process.env.XDG_CONFIG_HOME ?? join(homedir(), \".config\"), \"murmur\")\n );\n}\n\nexport function dbPath(): string {\n return join(stateDir(), \"events.db\");\n}\n","import { execFileSync } from \"node:child_process\";\nimport type { AgentState } from \"./types.js\";\n\nexport type Location = {\n session: string;\n window: string;\n pane: string;\n session_name: string | null;\n window_name: string | null;\n};\n\nexport interface Mux {\n currentWindow(): Location | null;\n liveWindows(): Set<string> | null;\n setState(window: string, state: AgentState | null): void;\n attach(session: string, window: string): void;\n windowNames(): Map<string, string>;\n windowForPane(pane: string): string | null;\n windowNamed(name: string): string | null;\n selectWindow(window: string): void;\n capture(pane: string, lines?: number): string | null;\n}\n\nfunction runTmux(args: string[]): string | null {\n try {\n return execFileSync(\"tmux\", args, {\n encoding: \"utf8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n } catch {\n return null;\n }\n}\n\nexport const tmux: Mux = {\n currentWindow() {\n const pane = process.env.TMUX_PANE ?? runTmux([\"display-message\", \"-p\", \"#{pane_id}\"]);\n if (!pane) return null;\n\n // One call for ids and names together. The names are recorded on every\n // event because a reader cannot resolve a remote window id against its own\n // tmux, so they have to travel with the event.\n const fields = runTmux([\n \"display-message\",\n \"-t\",\n pane,\n \"-p\",\n \"#{session_id}\\t#{window_id}\\t#{session_name}\\t#{window_name}\",\n ]);\n const [session, window, sessionName, windowName] = fields?.split(\"\\t\") ?? [];\n if (!session || !window) return null;\n return {\n session,\n window,\n pane,\n session_name: sessionName || null,\n window_name: windowName || null,\n };\n },\n\n // Which of this host's windows still exist. Only the authoring node can\n // answer this, which is why the check runs on export rather than on the\n // reader: a peer holding a `blocked` row for a window that died has nothing\n // to supersede it, and the agent stays in every HUD forever.\n //\n // null means \"could not tell\" (no tmux server, tmux missing) and is\n // deliberately distinct from an empty set, which means \"tmux answered, and\n // there are no windows\". Treating the first as the second would clear every\n // agent on the host the moment tmux was unreachable.\n liveWindows() {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\"]);\n if (out === null) return null;\n return new Set(out.split(\"\\n\").filter(Boolean));\n },\n\n setState(window, state) {\n if (state === null) {\n runTmux([\"set-window-option\", \"-qu\", \"-t\", window, \"@agent_state\"]);\n } else {\n runTmux([\"set-window-option\", \"-q\", \"-t\", window, \"@agent_state\", state]);\n runTmux([\"set-window-option\", \"-q\", \"-t\", window, \"@pane_agent\", \"1\"]);\n }\n runTmux([\"refresh-client\", \"-S\"]);\n },\n\n attach(session, window) {\n // Two steps, because switch-client alone is a no-op when the target window\n // is in the session you are already attached to — which is the common case\n // for a local agent, and why \"enter\" appeared to do nothing.\n // switch-client moves the client between sessions; select-window moves\n // that session to the right window.\n runTmux([\"switch-client\", \"-t\", session]);\n runTmux([\"select-window\", \"-t\", window]);\n },\n\n // Window ids are what the log stores, because they are stable; names are\n // what a human recognises in a picker. Names are live tmux state, not\n // history, so they are resolved at render time rather than recorded.\n windowNames() {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\\t#{window_name}\"]);\n const names = new Map<string, string>();\n for (const line of out?.split(\"\\n\") ?? []) {\n const [id, name] = line.split(\"\\t\");\n if (id && name) names.set(id, name);\n }\n return names;\n },\n\n // First window carrying this exact name, or null. Used to reuse a per-host\n // ssh window instead of opening another one.\n windowNamed(name) {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\\t#{window_name}\"]);\n for (const line of out?.split(\"\\n\") ?? []) {\n const [id, windowName] = line.split(\"\\t\");\n if (id && windowName === name) return id;\n }\n return null;\n },\n\n selectWindow(window) {\n runTmux([\"select-window\", \"-t\", window]);\n },\n\n // The window a pane belongs to, for a pane murmur has no event for. Clearing\n // a badge is a tmux operation and does not require murmur to own the pane.\n windowForPane(pane) {\n return runTmux([\"display-message\", \"-t\", pane, \"-p\", \"#{window_id}\"]) || null;\n },\n\n capture(pane, lines) {\n const args = [\"capture-pane\", \"-p\", \"-t\", pane];\n if (lines !== undefined) args.push(\"-S\", `-${lines}`);\n return runTmux(args);\n },\n};\n\nexport function pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code !== \"ESRCH\";\n }\n}\n","import { execFile, execFileSync } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\nconst CONTROL_PATH = \"~/.ssh/control/%r@%h:%p\";\n\n// A peer that is merely unreachable — asleep, off the VPN, a stale address —\n// must not hold up a command. OpenSSH's default TCP connect timeout is the\n// kernel's, which is 75s on macOS; at that point `murmur pick` is unusable and\n// the HUD tick overlaps itself. Two seconds is far above any real handshake on\n// a LAN or a VPN, and a peer that misses it simply shows stale, which is the\n// designed outcome for a host you cannot reach.\nconst CONNECT_TIMEOUT_S = 2;\n\n// Belt and braces for a host that completes the TCP connect and then stops\n// responding — ConnectTimeout does not cover that, and it is how a sleeping\n// laptop behaves. Bounds the whole exchange rather than just the dial.\nconst EXEC_TIMEOUT_MS = 10_000;\n\nconst SSH_OPTIONS = [\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"ControlMaster=no\",\n \"-o\",\n `ControlPath=${CONTROL_PATH}`,\n \"-o\",\n `ConnectTimeout=${CONNECT_TIMEOUT_S}`,\n];\n\nexport interface Channel {\n exec(target: string, argv: string[]): Promise<string>;\n}\n\nexport const ssh: Channel = {\n async exec(target, argv) {\n const { stdout } = await execFileAsync(\"ssh\", [...SSH_OPTIONS, target, ...argv], {\n encoding: \"utf8\",\n timeout: EXEC_TIMEOUT_MS,\n });\n return stdout;\n },\n};\n\nexport function hasWarmSocket(target: string): boolean {\n try {\n execFileSync(\"ssh\", [...SSH_OPTIONS, \"-O\", \"check\", target], { stdio: \"ignore\" });\n return true;\n } catch {\n return false;\n }\n}\n","export type AgentState = \"working\" | \"blocked\" | \"done\" | \"crashed\" | \"cleared\";\n\nexport type Driver = \"human\" | \"orchestrated\";\n\nexport const DEFAULT_DRIVER: Driver = \"human\";\n\nexport type Event = {\n host_id: string;\n seq: number;\n ts: number;\n agent_id: string;\n session: string;\n window: string;\n pane: string;\n // Human-readable names, recorded by the node that owns the pane. tmux ids are\n // stable and are what jumps; names are what a human recognises. They are\n // *recorded* rather than resolved at render time because a reader cannot look\n // a remote window id up in its own tmux -- doing so labelled a remote agent\n // with whatever this host had at that id. Cost: a renamed window keeps its\n // old name until the next event, which is the same property the history rows\n // always had.\n session_name: string | null;\n window_name: string | null;\n // The agent's own idea of what it is working on: pi's session name, and mu's\n // $MU_AGENT_NAME for an orchestrated agent. Both are richer than the window\n // name when they exist, and neither is derivable from tmux.\n agent_name: string | null;\n pi_session: string | null;\n workstream: string | null;\n role: string | null;\n cli: string | null;\n driver: Driver | null;\n kind: string;\n state: AgentState | string;\n message: string;\n pid: number | null;\n synthetic: boolean;\n reason: string;\n extra: Record<string, unknown>;\n};\n\nexport type Peer = {\n name: string;\n target: string;\n host_id: string | null;\n display_name: string | null;\n watermark: number;\n fetched_at: number | null;\n /** When a jump last found this peer's tmux server down. Null once it answers. */\n tmux_down_at: number | null;\n};\n","import { type AgentState, DEFAULT_DRIVER, type Driver, type Event } from \"./types.js\";\n\nexport type LiveCheck = (pid: number) => boolean;\n\nexport type AgentView = {\n agent_id: string;\n host_id: string;\n state: AgentState | null;\n event: Event | null;\n workstream: string | null;\n role: string | null;\n cli: string | null;\n driver: Driver;\n session: string;\n window: string;\n pane: string;\n // Names as recorded by the authoring node, so a remote agent is labelled by\n // its own host's tmux rather than by whatever this host has at that id.\n session_name: string | null;\n window_name: string | null;\n agent_name: string | null;\n pi_session: string | null;\n fetched_at: number | null;\n};\n\nexport function foldAgent(\n events: Event[],\n isAlive: LiveCheck,\n): { state: AgentState | null; event: Event | null } {\n for (let index = events.length - 1; index >= 0; index -= 1) {\n const event = events[index];\n if (!event) continue;\n\n switch (event.state) {\n case \"blocked\":\n case \"done\":\n case \"crashed\":\n return { state: event.state, event };\n case \"cleared\":\n return { state: null, event: null };\n case \"working\":\n return {\n state: event.pid !== null && event.pid > 0 && isAlive(event.pid) ? \"working\" : \"crashed\",\n event,\n };\n }\n }\n\n return { state: null, event: null };\n}\n\nexport function foldAll(events: Event[], isAlive: LiveCheck): AgentView[] {\n const byAgent = new Map<string, Event[]>();\n for (const event of events) {\n const agentEvents = byAgent.get(event.agent_id);\n if (agentEvents) agentEvents.push(event);\n else byAgent.set(event.agent_id, [event]);\n }\n\n return [...byAgent.values()].map((agentEvents) => {\n const folded = foldAgent(agentEvents, isAlive);\n const source = folded.event ?? agentEvents[agentEvents.length - 1];\n if (!source) throw new Error(\"agent event group cannot be empty\");\n\n return {\n agent_id: source.agent_id,\n host_id: source.host_id,\n state: folded.state,\n event: folded.event,\n workstream: source.workstream,\n role: source.role,\n cli: source.cli,\n driver: source.driver ?? DEFAULT_DRIVER,\n session: source.session,\n window: source.window,\n pane: source.pane,\n session_name: source.session_name,\n window_name: source.window_name,\n agent_name: source.agent_name,\n pi_session: source.pi_session,\n fetched_at: null,\n };\n });\n}\n\nconst ATTENTION_ORDER: Record<AgentState, number> = {\n blocked: 0,\n done: 1,\n crashed: 2,\n working: 3,\n cleared: 4,\n};\n\nexport function attentionSort(views: AgentView[]): AgentView[] {\n return [...views].sort((left, right) => {\n const stateOrder =\n (left.state === null ? 4 : ATTENTION_ORDER[left.state]) -\n (right.state === null ? 4 : ATTENTION_ORDER[right.state]);\n if (stateOrder !== 0) return stateOrder;\n return (right.event?.ts ?? 0) - (left.event?.ts ?? 0);\n });\n}\n\nexport function isStale(fetchedAt: number | null, now: number, thresholdMs = 60_000): boolean {\n return fetchedAt !== null && now - fetchedAt > thresholdMs;\n}\n","import { foldAgent, type LiveCheck } from \"./fold.js\";\nimport { ensureIdentity } from \"./identity.js\";\nimport type { Store } from \"./store.js\";\nimport type { Driver, Event } from \"./types.js\";\n\nexport const SCHEMA_VERSION = 2;\n\nexport type Envelope = {\n schema_version: number;\n host_id: string;\n display_name: string;\n exported_at: number;\n};\n\nconst EVENT_FIELDS = new Set([\n \"host_id\",\n \"seq\",\n \"ts\",\n \"agent_id\",\n \"session\",\n \"window\",\n \"pane\",\n \"session_name\",\n \"window_name\",\n \"agent_name\",\n \"pi_session\",\n \"workstream\",\n \"role\",\n \"cli\",\n \"driver\",\n \"kind\",\n \"state\",\n \"message\",\n \"pid\",\n \"synthetic\",\n \"reason\",\n]);\n\nfunction eventToWire(event: Event): Record<string, unknown> {\n const { extra, ...known } = event;\n return { ...extra, ...known };\n}\n\nexport function eventFromWire(wire: Record<string, unknown>): Event {\n const extra = Object.fromEntries(Object.entries(wire).filter(([key]) => !EVENT_FIELDS.has(key)));\n return {\n host_id: wire.host_id as string,\n seq: wire.seq as number,\n ts: wire.ts as number,\n agent_id: wire.agent_id as string,\n session: wire.session as string,\n window: wire.window as string,\n pane: wire.pane as string,\n session_name: (wire.session_name as string | null | undefined) ?? null,\n window_name: (wire.window_name as string | null | undefined) ?? null,\n agent_name: (wire.agent_name as string | null | undefined) ?? null,\n pi_session: (wire.pi_session as string | null | undefined) ?? null,\n workstream: (wire.workstream as string | null | undefined) ?? null,\n role: (wire.role as string | null | undefined) ?? null,\n cli: (wire.cli as string | null | undefined) ?? null,\n driver: (wire.driver as Driver | null | undefined) ?? null,\n kind: wire.kind as string,\n state: wire.state as string,\n message: wire.message as string,\n pid: (wire.pid as number | null | undefined) ?? null,\n synthetic: wire.synthetic as boolean,\n reason: wire.reason as string,\n extra,\n };\n}\n\nfunction synthesizeCrashes(store: Store, hostId: string, isAlive: LiveCheck): void {\n const byAgent = new Map<string, Event[]>();\n for (const event of store.allEvents()) {\n if (event.host_id !== hostId) continue;\n const events = byAgent.get(event.agent_id);\n if (events) events.push(event);\n else byAgent.set(event.agent_id, [event]);\n }\n\n for (const events of byAgent.values()) {\n events.sort((left, right) => left.seq - right.seq);\n const newest = events.at(-1);\n if (\n newest &&\n newest.state === \"working\" &&\n !newest.synthetic &&\n foldAgent(events, isAlive).state === \"crashed\"\n ) {\n const { host_id: _hostId, seq: _seq, ts: _ts, ...event } = newest;\n store.append({ ...event, state: \"crashed\", synthetic: true, reason: \"pid_gone\" });\n }\n }\n}\n\n/**\n * Clear agents whose tmux window is gone.\n *\n * A window that dies takes its agent with it, but the log's newest row still\n * says `blocked`, so every peer keeps showing an agent that cannot be jumped\n * to -- the fold has nothing to supersede that row with. Only the authoring\n * node can tell, which is why this runs on export beside crash synthesis\n * rather than on the reader.\n *\n * `cleared` is the right state: it already means \"no longer wants attention\"\n * and resets the fold to none. An appended event rather than an export-time\n * filter, so the fact replicates once and explains itself, instead of every\n * peer having to re-derive it from an absence.\n */\nexport function clearDeadWindows(store: Store, hostId: string, live: Set<string> | null): void {\n // null means tmux could not answer. An empty set means it did and there are\n // no windows. Conflating them would clear every agent on the host whenever\n // tmux was briefly unreachable.\n if (live === null) return;\n\n const newest = new Map<string, Event>();\n for (const event of store.allEvents()) {\n if (event.host_id !== hostId) continue;\n const previous = newest.get(event.agent_id);\n if (!previous || event.seq > previous.seq) newest.set(event.agent_id, event);\n }\n\n for (const event of newest.values()) {\n if (event.state === \"cleared\") continue;\n if (live.has(event.window)) continue;\n const { host_id: _hostId, seq: _seq, ts: _ts, ...rest } = event;\n store.append({\n ...rest,\n state: \"cleared\",\n synthetic: true,\n reason: \"window_gone\",\n message: \"\",\n });\n }\n}\n\nexport function exportJsonl(\n store: Store,\n since: number,\n isAlive: LiveCheck,\n live?: Set<string> | null,\n): string {\n const identity = ensureIdentity();\n synthesizeCrashes(store, identity.host_id, isAlive);\n if (live !== undefined) clearDeadWindows(store, identity.host_id, live);\n\n const envelope: Envelope = {\n schema_version: SCHEMA_VERSION,\n host_id: identity.host_id,\n display_name: identity.display_name,\n exported_at: Date.now(),\n };\n const lines = [\n JSON.stringify(envelope),\n ...store\n .eventsSince(identity.host_id, since)\n .map((event) => JSON.stringify(eventToWire(event))),\n ];\n return `${lines.join(\"\\n\")}\\n`;\n}\n","import type { Channel } from \"./channel.js\";\nimport { type Envelope, eventFromWire, SCHEMA_VERSION } from \"./export.js\";\nimport type { Store } from \"./store.js\";\nimport type { Event } from \"./types.js\";\n\nexport const COLLECT_INTERVAL_MS = 30_000;\nexport const STALENESS_MS = 2 * COLLECT_INTERVAL_MS;\n\nexport type CollectResult = {\n peer: string;\n ok: boolean;\n ingested: number;\n error?: string;\n};\n\nfunction parseJsonl(output: string): { envelope: Envelope; events: Event[] } {\n const lines = output.trim().split(\"\\n\");\n const envelope = JSON.parse(lines.shift() ?? \"\") as Envelope;\n if (envelope.schema_version > SCHEMA_VERSION) {\n throw new Error(\n `unsupported schema version ${envelope.schema_version} (supports ${SCHEMA_VERSION})`,\n );\n }\n return {\n envelope,\n events: lines.map((line) => eventFromWire(JSON.parse(line) as Record<string, unknown>)),\n };\n}\n\nexport async function collect(\n store: Store,\n channel: Channel,\n now = Date.now(),\n): Promise<CollectResult[]> {\n const results: CollectResult[] = [];\n try {\n for (const peer of store.peers()) {\n try {\n const output = await channel.exec(peer.target, [\n \"murmur\",\n \"export\",\n \"--since\",\n String(peer.watermark),\n ]);\n const { envelope, events } = parseJsonl(output);\n const ingested = store.ingest(events);\n const watermark = events\n .filter((event) => event.host_id === envelope.host_id)\n .reduce((highest, event) => Math.max(highest, event.seq), peer.watermark);\n store.upsertPeer({\n name: peer.name,\n target: peer.target,\n host_id: envelope.host_id,\n display_name: envelope.display_name,\n watermark,\n fetched_at: now,\n // New events mean the node is authoring again, so whatever a jump\n // observed about its tmux is out of date. Only clear on actual new\n // events: an export that returns nothing proves the binary ran, not\n // that tmux is back, which is the distinction that let a dead host\n // look healthy for three hours.\n tmux_down_at: ingested > 0 ? null : peer.tmux_down_at,\n });\n store.prune();\n results.push({ peer: peer.name, ok: true, ingested });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`murmur: collect: peer ${peer.name}: ${message}\\n`);\n results.push({ peer: peer.name, ok: false, ingested: 0, error: message });\n }\n }\n } catch (error) {\n process.stderr.write(\n `murmur: collect: ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n }\n return results;\n}\n","import { execFileSync } from \"node:child_process\";\nimport type { Agent } from \"./agents.js\";\nimport { loadIdentity } from \"./identity.js\";\nimport { tmux } from \"./mux.js\";\nimport type { Store } from \"./store.js\";\n\n/**\n * Glance: the last few lines a pane printed.\n *\n * This is the cheap half of the two things \"render any pane from the master\"\n * hides. It is a stateless `capture-pane`, not a frame stream — no resize\n * negotiation, no input routing, no reconnect. That deferral is what keeps\n * murmur a state layer instead of a multiplexer (DESIGN-NOTES, \"Deferring\n * interactive remote rendering\"), and it is why this file is thirty lines\n * rather than most of herdr.\n */\n\nconst GLANCE_LINES = 40;\n\n// Same posture as the collector: ride a warm socket or fail fast, never\n// prompt. A preview pane must not trigger a yubikey touch on every keypress.\nconst SSH_OPTIONS = [\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"ControlMaster=no\",\n \"-o\",\n \"ControlPath=~/.ssh/control/%r@%h:%p\",\n \"-o\",\n \"ConnectTimeout=2\",\n];\n\nexport function glance(store: Store, agent: Agent, lines = GLANCE_LINES): string | null {\n if (agent.host_id === loadIdentity()?.host_id) return tmux.capture(agent.pane, lines);\n\n const peer = store.peers().find((candidate) => candidate.host_id === agent.host_id);\n const target = peer?.target ?? peer?.name;\n if (!target) return null;\n try {\n // The pane id is `%N`, which a remote shell leaves alone, but quote it\n // anyway: the same class of bug as the `$N` session id that made remote\n // jump fail silently for a day.\n return execFileSync(\n \"ssh\",\n [\n ...SSH_OPTIONS,\n target,\n \"tmux\",\n \"capture-pane\",\n \"-p\",\n \"-t\",\n `'${agent.pane}'`,\n \"-S\",\n `-${lines}`,\n ],\n { encoding: \"utf8\", timeout: 3000, stdio: [\"ignore\", \"pipe\", \"ignore\"] },\n );\n } catch {\n // Unreachable, cold socket, dead tmux, gone pane. The preview says so\n // rather than the picker failing.\n return null;\n }\n}\n","import { ssh } from \"./channel.js\";\nimport { collect, STALENESS_MS } from \"./collector.js\";\nimport { type AgentView, attentionSort, foldAll, isStale } from \"./fold.js\";\nimport { loadIdentity } from \"./identity.js\";\nimport { pidAlive } from \"./mux.js\";\nimport type { Store } from \"./store.js\";\n\ntype StatusState = \"working\" | \"blocked\" | \"done\" | \"crashed\" | \"idle\";\ntype Counts = Record<StatusState, number>;\n\nexport type Status = {\n counts: Counts;\n orchestrated_counts: Counts;\n agents: (AgentView & {\n stale: boolean;\n age_ms: number | null;\n event_age_ms: number | null;\n tmux_down: boolean;\n host: string;\n })[];\n peers: {\n name: string;\n display_name: string | null;\n fetched_at: number | null;\n stale: boolean;\n }[];\n};\n\nfunction emptyCounts(): Counts {\n return { working: 0, blocked: 0, done: 0, crashed: 0, idle: 0 };\n}\n\nexport function tmuxStatus(view: Status): string {\n const urgency: StatusState[] = [\"crashed\", \"blocked\", \"done\", \"working\", \"idle\"];\n return urgency\n .filter((state) => view.counts[state] > 0)\n .map((state) => `${state}\\t${view.counts[state]}\\n`)\n .join(\"\");\n}\n\n/**\n * Fold the current view. Pure with respect to the network: the caller decides\n * whether to collect first (see `statusWithCollect`).\n */\nexport function status(store: Store, now = Date.now()): Status {\n const identity = loadIdentity();\n const peers = store.peers();\n const peersByHost = new Map(\n peers.flatMap((peer) => (peer.host_id === null ? [] : [[peer.host_id, peer] as const])),\n );\n const events = store.allEvents();\n const local = foldAll(\n events.filter((event) => event.host_id === identity?.host_id),\n pidAlive,\n );\n const remote = foldAll(\n events.filter((event) => event.host_id !== identity?.host_id),\n () => true,\n );\n const counts = emptyCounts();\n const orchestratedCounts = emptyCounts();\n const agents = attentionSort([...local, ...remote]).map((agent) => {\n const peer = peersByHost.get(agent.host_id);\n const fetchedAt = peer?.fetched_at ?? null;\n const state: StatusState =\n agent.state === null || agent.state === \"cleared\" ? \"idle\" : agent.state;\n const target = agent.driver === \"human\" ? counts : orchestratedCounts;\n target[state] += 1;\n return {\n ...agent,\n fetched_at: fetchedAt,\n // Replica freshness: how long since we last reached the peer. Local rows\n // have no fetched_at and are never stale.\n stale: isStale(fetchedAt, now, STALENESS_MS),\n age_ms: fetchedAt === null ? null : now - fetchedAt,\n // Information age: how long since the agent itself said anything. This\n // is the number a human means by \"how stale is that row\". A successful\n // fetch of a three-hour-old event resets age_ms to zero but leaves this\n // at three hours, which is why they cannot be the same field.\n event_age_ms: agent.event === null ? null : Math.max(0, now - agent.event.ts),\n // A jump proved this host's tmux was down and nothing has authored since.\n // Stronger than staleness: the host answers, its agents are just gone.\n tmux_down: peer?.tmux_down_at != null,\n host:\n peer?.display_name ??\n peer?.name ??\n (agent.host_id === identity?.host_id ? identity.display_name : agent.host_id),\n };\n });\n\n return {\n counts,\n orchestrated_counts: orchestratedCounts,\n agents,\n peers: peers.map((peer) => ({\n name: peer.name,\n display_name: peer.display_name,\n fetched_at: peer.fetched_at,\n // A peer we have never reached is stale, not fresh. `isStale` reads a\n // null `fetched_at` as \"local, therefore never stale\", which is right\n // for an agent row but backwards for a peer: null there means the very\n // first collect has not succeeded yet. Left to `isStale`, an\n // unreachable host you just added would render as up to date.\n stale: peer.fetched_at === null || isStale(peer.fetched_at, now, STALENESS_MS),\n })),\n };\n}\n\n/**\n * Collect from peers, then fold. This is what every user-facing surface wants:\n * the view reflects the sync that just ran, rather than the one before it.\n *\n * Awaiting matters for two reasons. A fire-and-forget collect makes every\n * invocation show data one run stale — you never see what you just fetched.\n * And the callers close the store in a `finally`, so a collect still in flight\n * lands on a closed handle and reports \"The database connection is not open\",\n * which looks like corruption rather than a race.\n *\n * Sync must never fail a command, so a peer failure only warns. With no peers\n * this is a loop over an empty array: no network, no added latency, which is\n * the everyday single-machine path.\n */\nexport async function statusWithCollect(store: Store, now = Date.now()): Promise<Status> {\n try {\n await collect(store, ssh, now);\n } catch (error) {\n process.stderr.write(\n `murmur: status: collect: ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n }\n return status(store, now);\n}\n","import { rmSync } from \"node:fs\";\nimport Database from \"better-sqlite3\";\nimport { ensureIdentity } from \"./identity.js\";\nimport { dbPath } from \"./paths.js\";\nimport type { Driver, Event, Peer } from \"./types.js\";\n\nconst DEFAULT_RETENTION_MS = 7 * 86_400_000;\n\n/**\n * Local storage shape. Bump on any change to the events or peers tables.\n *\n * Distinct from `SCHEMA_VERSION` in export.ts, which versions the *wire*: a\n * node can change how it stores events without changing what it sends, and a\n * wire change should not throw away local history.\n */\nexport const STORE_VERSION = 2;\n\n/**\n * Migration strategy: there isn't one. A version mismatch deletes the database\n * and starts again.\n *\n * This is only acceptable because nothing in events.db is authoritative or\n * irreplaceable. It is a bounded-retention observability log: remote events\n * re-sync from their authoring peer on the next collect, local agents re-report\n * on their next state change, and node identity deliberately lives in a\n * separate file. If anything durable is ever added here, this stops being safe\n * and a real migration is required.\n *\n * Peers survive, because they are the one thing a human typed. Watermarks are\n * reset with the events they indexed -- keeping them would skip the events the\n * new database no longer has -- and re-reading a peer from zero is free, since\n * ingest is idempotent.\n */\nfunction resetIfStale(path: string): Peer[] {\n let salvaged: Peer[] = [];\n try {\n const existing = new Database(path, { fileMustExist: true });\n const version = (existing.pragma(\"user_version\", { simple: true }) as number) ?? 0;\n if (version === STORE_VERSION) {\n existing.close();\n return salvaged;\n }\n try {\n salvaged = existing\n .prepare(\"SELECT name, target, host_id, display_name FROM peers\")\n .all() as Peer[];\n } catch {\n // Old enough not to have the table, or unreadable. Nothing to save.\n }\n existing.close();\n } catch {\n // No database yet, or one too broken to open. Either way, recreate.\n return salvaged;\n }\n\n // -wal and -shm must go too: a stale sidecar against a fresh main file is a\n // documented way to corrupt sqlite.\n for (const suffix of [\"\", \"-wal\", \"-shm\"]) rmSync(`${path}${suffix}`, { force: true });\n return salvaged;\n}\n\n// The name fields are optional on the way in: a caller that has no name for a\n// thing should not have to say `null` four times, and a non-tmux harness has\n// none of them. They are non-optional on `Event` itself, so a reader never has\n// to distinguish absent from null.\nexport type NewEvent = Omit<\n Event,\n \"host_id\" | \"seq\" | \"ts\" | \"session_name\" | \"window_name\" | \"agent_name\" | \"pi_session\"\n> & {\n ts?: number;\n session_name?: string | null;\n window_name?: string | null;\n agent_name?: string | null;\n pi_session?: string | null;\n};\n\ntype EventRow = Omit<Event, \"synthetic\" | \"extra\"> & {\n synthetic: number;\n extra: string;\n};\n\nfunction eventValues(event: Event): unknown[] {\n return [\n event.host_id,\n event.seq,\n event.ts,\n event.agent_id,\n event.session,\n event.window,\n event.pane,\n event.session_name,\n event.window_name,\n event.agent_name,\n event.pi_session,\n event.workstream,\n event.role,\n event.cli,\n event.driver,\n event.kind,\n event.state,\n event.message,\n event.pid,\n Number(event.synthetic),\n event.reason,\n JSON.stringify(event.extra),\n ];\n}\n\nfunction toEvent(row: EventRow): Event {\n return {\n ...row,\n driver: row.driver as Driver | null,\n synthetic: row.synthetic === 1,\n extra: JSON.parse(row.extra) as Record<string, unknown>,\n };\n}\n\nexport interface Store {\n append(event: NewEvent): Event;\n ingest(events: Event[]): number;\n eventsSince(hostId: string, seq: number): Event[];\n allEvents(): Event[];\n maxSeq(hostId: string): number;\n prune(horizonMs?: number): number;\n peers(): Peer[];\n /**\n * Drop every event for one agent from this node's replica.\n *\n * For a remote agent this is a replica eviction, not a claim about truth: the\n * authoring node still owns it, and a collect re-reads from the watermark if\n * it is still alive.\n */\n forgetAgent(agentId: string): number;\n forgetHost(hostId: string): number;\n upsertPeer(peer: Partial<Peer> & { name: string; target: string }): void;\n removePeer(name: string): boolean;\n close(): void;\n}\n\nexport function openStore(): Store {\n const identity = ensureIdentity();\n const path = dbPath();\n const salvagedPeers = resetIfStale(path);\n const database = new Database(path);\n database.pragma(\"journal_mode = WAL\");\n database.pragma(`user_version = ${STORE_VERSION}`);\n database.exec(`\n CREATE TABLE IF NOT EXISTS events (\n host_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n ts INTEGER NOT NULL,\n agent_id TEXT NOT NULL,\n session TEXT NOT NULL,\n window TEXT NOT NULL,\n pane TEXT NOT NULL,\n session_name TEXT,\n window_name TEXT,\n agent_name TEXT,\n pi_session TEXT,\n workstream TEXT,\n role TEXT,\n cli TEXT,\n driver TEXT,\n kind TEXT NOT NULL,\n state TEXT NOT NULL,\n message TEXT NOT NULL,\n pid INTEGER,\n synthetic INTEGER NOT NULL,\n reason TEXT NOT NULL,\n extra TEXT NOT NULL,\n PRIMARY KEY (host_id, seq)\n );\n CREATE INDEX IF NOT EXISTS events_agent_seq ON events (agent_id, seq);\n CREATE TABLE IF NOT EXISTS peers (\n name TEXT PRIMARY KEY,\n target TEXT NOT NULL,\n host_id TEXT,\n display_name TEXT,\n watermark INTEGER NOT NULL,\n fetched_at INTEGER,\n -- When a jump last proved this peer's tmux was not answering. Reader\n -- state, not an event: this node cannot author facts about another\n -- node's agents, and a jump is a local observation, not something the\n -- peer said. Cleared by the next successful collect.\n tmux_down_at INTEGER\n );\n `);\n\n // Additive migration: an existing peers table predates tmux_down_at.\n try {\n database.exec(\"ALTER TABLE peers ADD COLUMN tmux_down_at INTEGER\");\n } catch {\n // Already present.\n }\n\n // Put back the peers the wipe took, at watermark 0 so the next collect\n // re-reads each one from the start.\n if (salvagedPeers.length > 0) {\n const restore = database.prepare(\n `INSERT OR IGNORE INTO peers (name, target, host_id, display_name, watermark, fetched_at)\n VALUES (?, ?, ?, ?, 0, NULL)`,\n );\n for (const peer of salvagedPeers) {\n restore.run(peer.name, peer.target, peer.host_id ?? null, peer.display_name ?? null);\n }\n }\n\n const eventColumns = `\n host_id, seq, ts, agent_id, session, window, pane,\n session_name, window_name, agent_name, pi_session,\n workstream, role, cli, driver, kind, state, message, pid,\n synthetic, reason, extra`;\n const eventPlaceholders = new Array(22).fill(\"?\").join(\", \");\n const insertEvent = database.prepare(\n `INSERT INTO events (${eventColumns}) VALUES (${eventPlaceholders})`,\n );\n const ingestEvent = database.prepare(\n `INSERT OR IGNORE INTO events (${eventColumns}) VALUES (${eventPlaceholders})`,\n );\n const selectMaxSeq = database.prepare(\n \"SELECT COALESCE(MAX(seq), 0) AS seq FROM events WHERE host_id = ?\",\n );\n const append = database.transaction((event: NewEvent): Event => {\n const row = selectMaxSeq.get(identity.host_id) as { seq: number };\n const stored: Event = {\n ...event,\n host_id: identity.host_id,\n seq: row.seq + 1,\n ts: event.ts ?? Date.now(),\n session_name: event.session_name ?? null,\n window_name: event.window_name ?? null,\n agent_name: event.agent_name ?? null,\n pi_session: event.pi_session ?? null,\n };\n insertEvent.run(...eventValues(stored));\n return stored;\n });\n const ingest = database.transaction((events: Event[]): number => {\n let inserted = 0;\n for (const event of events) inserted += ingestEvent.run(...eventValues(event)).changes;\n return inserted;\n });\n\n return {\n append,\n ingest,\n eventsSince(hostId, seq) {\n const rows = database\n .prepare(\"SELECT * FROM events WHERE host_id = ? AND seq > ? ORDER BY seq\")\n .all(hostId, seq) as EventRow[];\n return rows.map(toEvent);\n },\n allEvents() {\n const rows = database\n .prepare(\"SELECT * FROM events ORDER BY ts, host_id, seq\")\n .all() as EventRow[];\n return rows.map(toEvent);\n },\n maxSeq(hostId) {\n return (selectMaxSeq.get(hostId) as { seq: number }).seq;\n },\n prune(horizonMs = Number(process.env.MURMUR_RETENTION_MS ?? DEFAULT_RETENTION_MS)) {\n return database\n .prepare(`\n DELETE FROM events\n WHERE ts < ?\n AND (host_id, seq) NOT IN (\n SELECT host_id, seq FROM (\n SELECT host_id, seq,\n ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY ts DESC, seq DESC) rn\n FROM events\n ) WHERE rn = 1\n )\n `)\n .run(Date.now() - horizonMs).changes;\n },\n peers() {\n return database.prepare(\"SELECT * FROM peers ORDER BY name\").all() as Peer[];\n },\n forgetAgent(agentId) {\n return database.prepare(\"DELETE FROM events WHERE agent_id = ?\").run(agentId).changes;\n },\n forgetHost(hostId) {\n // Every replicated row for one origin node. Only ever called about a\n // REMOTE host: the local host's rows are this node's own authorship and\n // the retention horizon owns them.\n return database.prepare(\"DELETE FROM events WHERE host_id = ?\").run(hostId).changes;\n },\n upsertPeer(peer) {\n const current = database.prepare(\"SELECT * FROM peers WHERE name = ?\").get(peer.name) as\n | Peer\n | undefined;\n database\n .prepare(`\n INSERT INTO peers (name, target, host_id, display_name, watermark, fetched_at, tmux_down_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n target = excluded.target,\n host_id = excluded.host_id,\n display_name = excluded.display_name,\n watermark = excluded.watermark,\n fetched_at = excluded.fetched_at,\n tmux_down_at = excluded.tmux_down_at\n `)\n .run(\n peer.name,\n peer.target,\n peer.host_id !== undefined ? peer.host_id : (current?.host_id ?? null),\n peer.display_name !== undefined ? peer.display_name : (current?.display_name ?? null),\n peer.watermark !== undefined ? peer.watermark : (current?.watermark ?? 0),\n peer.fetched_at !== undefined ? peer.fetched_at : (current?.fetched_at ?? null),\n peer.tmux_down_at !== undefined ? peer.tmux_down_at : (current?.tmux_down_at ?? null),\n );\n },\n removePeer(name) {\n // Drops the peer and its watermark. Replicated events stay: they are\n // real history authored elsewhere, and the retention horizon already\n // ages them out. Re-adding the peer re-syncs from zero, which ingest\n // makes free.\n return database.prepare(\"DELETE FROM peers WHERE name = ?\").run(name).changes > 0;\n },\n close() {\n database.close();\n },\n };\n}\n","// SDK entry. package.json advertises this as the \".\" export, so anything a\n// consumer needs to drive murmur without shelling out to the CLI belongs here.\n// The CLI is a thin layer over exactly these units.\nexport const VERSION = \"0.1.0\";\n\nexport {\n type Agent,\n agentLabel,\n agentLocation,\n type JumpResult,\n jumpToAgent,\n shellQuote,\n} from \"./agents.js\";\nexport { type Channel, hasWarmSocket, ssh } from \"./channel.js\";\nexport {\n COLLECT_INTERVAL_MS,\n type CollectResult,\n collect,\n STALENESS_MS,\n} from \"./collector.js\";\nexport { eventFromWire, exportJsonl, SCHEMA_VERSION } from \"./export.js\";\nexport {\n type AgentView,\n attentionSort,\n foldAgent,\n foldAll,\n isStale,\n type LiveCheck,\n} from \"./fold.js\";\nexport { glance } from \"./glance.js\";\nexport { ensureIdentity, loadIdentity, type NodeIdentity } from \"./identity.js\";\nexport { type Mux, pidAlive, tmux } from \"./mux.js\";\nexport { configDir, dbPath, stateDir } from \"./paths.js\";\nexport { type Status, status } from \"./status.js\";\nexport { type NewEvent, openStore, STORE_VERSION, type Store } from \"./store.js\";\nexport {\n type AgentState,\n DEFAULT_DRIVER,\n type Driver,\n type Event,\n type Peer,\n} from \"./types.js\";\n"],"mappings":"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","names":["join","join","execFileSync","execFileSync","SSH_OPTIONS","execFileSync"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@martintrojer/murmur",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent state across every machine you work on, in one view.",
|
|
5
|
+
"publishConfig": { "access": "public" },
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Martin Trojer <martin.trojer@gmail.com>",
|
|
9
|
+
"homepage": "https://github.com/martintrojer/murmur",
|
|
10
|
+
"repository": { "type": "git", "url": "git+https://github.com/martintrojer/murmur.git" },
|
|
11
|
+
"keywords": ["tmux", "agents", "ai", "pi-coding-agent", "pi-package", "ssh"],
|
|
12
|
+
"engines": { "node": ">=20" },
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
17
|
+
"./extension-store": "./dist/extension/store.js"
|
|
18
|
+
},
|
|
19
|
+
"bin": { "murmur": "./dist/cli.js" },
|
|
20
|
+
"files": ["dist", "README.md", "ARCHITECTURE.md"],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"lint": "biome check --error-on-warnings src test",
|
|
25
|
+
"lint:fix": "biome check --write src test",
|
|
26
|
+
"format": "biome format --write src test",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"check": "npm run typecheck && npm run lint && npm run test",
|
|
29
|
+
"prepare": "npm run build && node scripts/install-hooks.mjs"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"better-sqlite3": "^13.0.2",
|
|
33
|
+
"commander": "^15.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@biomejs/biome": "^2.5.6",
|
|
37
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
38
|
+
"@types/node": "^22.10.0",
|
|
39
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
40
|
+
"tsup": "^8.3.5",
|
|
41
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
42
|
+
"vitest": "^4.1.10"
|
|
43
|
+
}
|
|
44
|
+
}
|