@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
JavaScript
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