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.

120 lines • 4.52 kB
import crypto from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; /** MUST equal credential-store's hash and check-on-stop.lib.mjs credHashForCwd. */ export function hashCwd(cwd) { return crypto .createHash("sha256") .update(path.resolve(cwd)) .digest("hex") .slice(0, 16); } /** `~/.cogent/agent-sessions/` — the learned-record directory. */ export function agentSessionsDir() { return path.join(os.homedir(), ".cogent", "agent-sessions"); } /** * `~/.cogent/agent-sessions/<hash(cwd)>.<platform>.json`. * * Keyed by platform as well as cwd because one directory can host both a Claude * Code and a Codex agent (that is the normal two-agent Cogent setup) and their * transcripts live in completely different stores. */ export function agentSessionPath(cwd, platform) { return path.join(agentSessionsDir(), `${hashCwd(cwd)}.${platform}.json`); } /** * Recover the config ROOT from an absolute transcript path. * * Claude Code lays transcripts out as `<root>/projects/<encoded-cwd>/<id>.jsonl`, * so the root is whatever sits above the `projects` segment — recovered by * structure, never by matching a known prefix, which is what lets a completely * unknown custom location teach us its root. * * Returns null when the shape does not match (a Codex rollout, a temp file, an * agent that changes its layout) — callers must treat the root as optional. */ export function rootFromTranscriptPath(transcriptPath) { if (!transcriptPath) return null; const parts = path.resolve(transcriptPath).split(path.sep); // …/<root>/projects/<encoded>/<id>.jsonl → need "projects" with a parent. const i = parts.lastIndexOf("projects"); if (i <= 0) return null; const root = parts.slice(0, i).join(path.sep); return root || path.sep; } /** Read the learned record for a cwd, or null. Never throws. */ export async function readAgentSession(cwd, platform) { try { const raw = await fs.readFile(agentSessionPath(cwd, platform), "utf-8"); const rec = JSON.parse(raw); // A record without the two fields that make it useful is not a record. if (!rec?.sessionId || !rec?.transcriptPath) return null; return rec; } catch { return null; } } /** * Persist a learned record. Atomic (temp + rename) so a hook and the bridge * writing concurrently can never leave a half-written file for the other to * read — the same discipline peer-registry uses for shared state. * Never throws: learning is an optimisation, never a reason to fail a wake. */ export async function writeAgentSession(rec) { try { const full = { ...rec, cwd: path.resolve(rec.cwd), root: rec.root ?? rootFromTranscriptPath(rec.transcriptPath) ?? undefined, updatedAt: new Date().toISOString(), }; const target = agentSessionPath(full.cwd, full.platform); await fs.mkdir(path.dirname(target), { recursive: true }); const tmp = `${target}.${process.pid}.tmp`; await fs.writeFile(tmp, JSON.stringify(full, null, 2), "utf-8"); await fs.rename(tmp, target); return true; } catch { return false; } } /** * Every distinct config root this machine has ever taught us, newest first. * * This is what turns one lucky resolution into permanent knowledge: a user with * `CLAUDE_CONFIG_DIR=/data/claude` teaches us that root once, and afterwards * EVERY project on the machine resolves against it — including ones opened in a * shell where the variable was never exported. Bounded work: one readdir plus a * small number of tiny JSON reads. */ export async function learnedRoots(platform) { try { const dir = agentSessionsDir(); const files = (await fs.readdir(dir)).filter((f) => f.endsWith(`.${platform}.json`)); const recs = await Promise.all(files.map(async (f) => { try { return JSON.parse(await fs.readFile(path.join(dir, f), "utf-8")); } catch { return null; } })); return [ ...new Set(recs .filter((r) => Boolean(r?.root)) .sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1)) .map((r) => r.root)), ]; } catch { return []; } } //# sourceMappingURL=agent-sessions.js.map