Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ and the lessons learned across every project — automatically.
fast cold starts.
- 🔧 **Focused status skill** — `$supermemory-status` checks authentication and connectivity;
memory operations come from MCP instead of separate command skills.
- ◪ **Persistent CLI mark** — compatible Codex terminals keep a quiet Supermemory badge
at the bottom of the TUI, while hook notices report live recall and save activity.

## Quick start

Expand Down Expand Up @@ -63,6 +65,11 @@ The installer:
- Registers the hooks in `~/.codex/hooks.json`
- Copies pre-bundled hook scripts to `~/.codex/supermemory/`
- Installs only the `supermemory-status` skill to `~/.codex/skills/`
- Installs a static custom TUI badge to `~/.codex/pets/supermemory/`

The installer selects the badge only when no Codex pet preference already exists. Terminals
without a supported inline-image protocol may not render it; recall and capture continue to work.
Use Codex's `/pet` picker to disable or change the persistent badge.

The hooks are tolerant: if Supermemory is unreachable, the API key is missing, or
anything else fails, they exit cleanly without breaking your Codex session.
Expand Down
168 changes: 168 additions & 0 deletions build.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as esbuild from "esbuild";
import { mkdirSync, writeFileSync, chmodSync, copyFileSync, readFileSync, rmSync } from "node:fs";
import { deflateSync } from "node:zlib";

const packageJson = JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf-8")
Expand Down Expand Up @@ -79,6 +80,173 @@ for (const skillName of ["supermemory-status"]) {
);
}

// Codex custom TUI pets use a fixed 8x9 spritesheet. Every frame in this
// sheet is intentionally identical: Supermemory needs a persistent activity
// badge, not an animated mascot that competes with the coding surface.
const PET_FRAME_WIDTH = 192;
const PET_FRAME_HEIGHT = 208;
const PET_COLUMNS = 8;
const PET_ROWS = 9;

const PET_FONT = {
E: ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
M: ["10001", "11011", "10101", "10101", "10001", "10001", "10001"],
O: ["01110", "10001", "10001", "10001", "10001", "10001", "01110"],
P: ["11110", "10001", "10001", "11110", "10000", "10000", "10000"],
R: ["11110", "10001", "10001", "11110", "10100", "10010", "10001"],
S: ["01111", "10000", "10000", "01110", "00001", "00001", "11110"],
U: ["10001", "10001", "10001", "10001", "10001", "10001", "01110"],
Y: ["10001", "10001", "01010", "00100", "00100", "00100", "00100"],
};

function crc32(buffer) {
let crc = 0xffffffff;
for (const byte of buffer) {
crc ^= byte;
for (let bit = 0; bit < 8; bit += 1) {
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
}
}
return (crc ^ 0xffffffff) >>> 0;
}

function pngChunk(type, data) {
const typeBuffer = Buffer.from(type, "ascii");
const length = Buffer.alloc(4);
length.writeUInt32BE(data.length);
const checksum = Buffer.alloc(4);
checksum.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])));
return Buffer.concat([length, typeBuffer, data, checksum]);
}

function setPixel(pixels, width, x, y, color) {
if (x < 0 || y < 0 || x >= width || y >= PET_FRAME_HEIGHT * PET_ROWS) return;
const offset = (y * width + x) * 4;
pixels[offset] = color[0];
pixels[offset + 1] = color[1];
pixels[offset + 2] = color[2];
pixels[offset + 3] = color[3];
}

function fillRect(pixels, width, x, y, rectWidth, rectHeight, color) {
for (let py = y; py < y + rectHeight; py += 1) {
for (let px = x; px < x + rectWidth; px += 1) {
setPixel(pixels, width, px, py, color);
}
}
}

function fillRoundedRect(pixels, width, x, y, rectWidth, rectHeight, radius, color) {
const right = x + rectWidth - 1;
const bottom = y + rectHeight - 1;
for (let py = y; py <= bottom; py += 1) {
for (let px = x; px <= right; px += 1) {
const nearestX = Math.max(x + radius, Math.min(px, right - radius));
const nearestY = Math.max(y + radius, Math.min(py, bottom - radius));
const dx = px - nearestX;
const dy = py - nearestY;
if (dx * dx + dy * dy <= radius * radius) {
setPixel(pixels, width, px, py, color);
}
}
}
}

function drawText(pixels, width, text, x, y, scale, color) {
let cursorX = x;
for (const character of text) {
const glyph = PET_FONT[character];
if (!glyph) continue;
glyph.forEach((row, rowIndex) => {
[...row].forEach((value, columnIndex) => {
if (value === "1") {
fillRect(
pixels,
width,
cursorX + columnIndex * scale,
y + rowIndex * scale,
scale,
scale,
color,
);
}
});
});
cursorX += 6 * scale;
}
}

function drawPetFrame(pixels, sheetWidth, frameX, frameY) {
const badgeX = frameX + 6;
const badgeY = frameY + 164;
fillRoundedRect(pixels, sheetWidth, badgeX, badgeY, 180, 36, 10, [24, 24, 27, 235]);
fillRoundedRect(pixels, sheetWidth, badgeX + 10, badgeY + 9, 18, 18, 3, [139, 124, 255, 255]);

// A tiny diagonal cut inside the square echoes the mark used by hook notices.
for (let row = 0; row < 12; row += 1) {
for (let column = row; column < 12; column += 1) {
setPixel(
pixels,
sheetWidth,
badgeX + 13 + column,
badgeY + 12 + row,
[242, 240, 255, 255],
);
}
}

drawText(
pixels,
sheetWidth,
"SUPERMEMORY",
badgeX + 36,
badgeY + 11,
2,
[226, 222, 255, 255],
);
}

function writePetSpritesheet(outputPath) {
const width = PET_FRAME_WIDTH * PET_COLUMNS;
const height = PET_FRAME_HEIGHT * PET_ROWS;
const pixels = Buffer.alloc(width * height * 4);

for (let row = 0; row < PET_ROWS; row += 1) {
for (let column = 0; column < PET_COLUMNS; column += 1) {
drawPetFrame(
pixels,
width,
column * PET_FRAME_WIDTH,
row * PET_FRAME_HEIGHT,
);
}
}

const scanlines = Buffer.alloc((width * 4 + 1) * height);
for (let y = 0; y < height; y += 1) {
const rowOffset = y * (width * 4 + 1);
scanlines[rowOffset] = 0;
pixels.copy(scanlines, rowOffset + 1, y * width * 4, (y + 1) * width * 4);
}

const header = Buffer.alloc(13);
header.writeUInt32BE(width, 0);
header.writeUInt32BE(height, 4);
header[8] = 8;
header[9] = 6;
const png = Buffer.concat([
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
pngChunk("IHDR", header),
pngChunk("IDAT", deflateSync(scanlines, { level: 9 })),
pngChunk("IEND", Buffer.alloc(0)),
]);
writeFileSync(outputPath, png);
}

mkdirSync("dist/pet", { recursive: true });
copyFileSync("src/pet/pet.json", "dist/pet/pet.json");
writePetSpritesheet("dist/pet/spritesheet.png");

// The root package.json declares `"type": "module"`, but esbuild emits CommonJS.
// Drop a CJS marker into dist/ so Node loads the bundles correctly.
mkdirSync("dist", { recursive: true });
Expand Down
91 changes: 84 additions & 7 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ const MCP_PROXY_SCRIPT = join(SUPERMEMORY_HOOKS_DIR, "mcp-proxy.js");
const FLUSH_SCRIPT = join(SUPERMEMORY_HOOKS_DIR, "flush.js");
const SESSION_START_SCRIPT = join(SUPERMEMORY_HOOKS_DIR, "session-start.js");
const CODEX_SKILLS_DIR = join(homedir(), ".codex", "skills");
const CODEX_PETS_DIR = join(CODEX_DIR, "pets");
const SUPERMEMORY_PET_DIR = join(CODEX_PETS_DIR, "supermemory");
const SUPERMEMORY_PET_MARKER = join(SUPERMEMORY_PET_DIR, ".codex-supermemory-owned");
const SUPERMEMORY_PET_ID = "supermemory";
const RECALL_TIMEOUT_SECONDS = 5;
const RECALL_APPROVE_TIMEOUT_SECONDS = 5;
const FLUSH_TIMEOUT_SECONDS = 30;
Expand Down Expand Up @@ -75,6 +79,7 @@ const LEGACY_SKILLS = [

const SCRIPT_DIR = getScriptDir();
const DIST_HOOKS_DIR = join(SCRIPT_DIR, "hooks");
const DIST_PET_DIR = join(SCRIPT_DIR, "pet");

function configParseError(filePath: string, parser: string, cause: unknown): Error {
const detail = cause instanceof Error ? cause.message : String(cause);
Expand Down Expand Up @@ -124,13 +129,42 @@ function readHooksJson(): HookEvents {
}
}

function mergeConfigToml(enable: boolean) {
function ownsSupermemoryPet(): boolean {
return existsSync(SUPERMEMORY_PET_MARKER);
}

function installPetAssets(): boolean {
if (existsSync(SUPERMEMORY_PET_DIR) && !ownsSupermemoryPet()) {
console.warn(
`! Kept existing unowned pet directory at ${SUPERMEMORY_PET_DIR}`,
);
return false;
}

mkdirSync(SUPERMEMORY_PET_DIR, { recursive: true });
copyFileSync(join(DIST_PET_DIR, "pet.json"), join(SUPERMEMORY_PET_DIR, "pet.json"));
copyFileSync(
join(DIST_PET_DIR, "spritesheet.png"),
join(SUPERMEMORY_PET_DIR, "spritesheet.png"),
);
writeFileSync(SUPERMEMORY_PET_MARKER, "codex-supermemory\n");
return true;
}

function removePetAssets(): void {
if (ownsSupermemoryPet()) {
rmSync(SUPERMEMORY_PET_DIR, { recursive: true, force: true });
}
}

function mergeConfigToml(enable: boolean, managePet: boolean): boolean {
if (!enable && !existsSync(CODEX_CONFIG_TOML)) {
// Nothing to disable — file doesn't exist yet.
return;
return false;
}

const config = readConfigToml();
let persistentIndicatorEnabled = false;

// Hooks are enabled by default in current Codex. Remove only the deprecated
// alias written by older codex-supermemory releases; preserve any explicit
Expand All @@ -148,6 +182,19 @@ function mergeConfigToml(enable: boolean) {
command: "node",
args: [MCP_PROXY_SCRIPT],
};

if (managePet) {
if (!config.tui) config.tui = {};
const tui = config.tui as Record<string, unknown>;
const hasPetSelection = Object.prototype.hasOwnProperty.call(tui, "pet");
if (!hasPetSelection) {
tui.pet = SUPERMEMORY_PET_ID;
tui.pet_anchor = "screen-bottom";
} else if (tui.pet === SUPERMEMORY_PET_ID && tui.pet_anchor === undefined) {
tui.pet_anchor = "screen-bottom";
}
persistentIndicatorEnabled = tui.pet === SUPERMEMORY_PET_ID;
}
} else {
const mcpServers = config.mcp_servers as Record<string, unknown> | undefined;
const server = mcpServers?.supermemory as Record<string, unknown> | undefined;
Expand All @@ -160,9 +207,17 @@ function mergeConfigToml(enable: boolean) {
if (mcpServers) delete mcpServers.supermemory;
if (mcpServers && Object.keys(mcpServers).length === 0) delete config.mcp_servers;
}

const tui = config.tui as Record<string, unknown> | undefined;
if (managePet && tui?.pet === SUPERMEMORY_PET_ID) {
delete tui.pet;
if (tui.pet_anchor === "screen-bottom") delete tui.pet_anchor;
if (Object.keys(tui).length === 0) delete config.tui;
}
}

writeFileSync(CODEX_CONFIG_TOML, TOML.stringify(config as TOML.JsonMap));
return persistentIndicatorEnabled;
}

interface HookEntry {
Expand Down Expand Up @@ -376,15 +431,19 @@ function install() {
const mcpProxySrc = join(DIST_HOOKS_DIR, "mcp-proxy.js");
const flushSrc = join(DIST_HOOKS_DIR, "flush.js");
const sessionStartSrc = join(DIST_HOOKS_DIR, "session-start.js");
const petManifestSrc = join(DIST_PET_DIR, "pet.json");
const petSpritesheetSrc = join(DIST_PET_DIR, "spritesheet.png");

if (
!existsSync(recallSrc) ||
!existsSync(recallApproveSrc) ||
!existsSync(mcpProxySrc) ||
!existsSync(flushSrc) ||
!existsSync(sessionStartSrc)
!existsSync(sessionStartSrc) ||
!existsSync(petManifestSrc) ||
!existsSync(petSpritesheetSrc)
) {
console.error("Error: Hook scripts not found. Please reinstall the package.");
console.error("Error: Installation assets not found. Please reinstall the package.");
process.exit(1);
}

Expand Down Expand Up @@ -423,9 +482,17 @@ function install() {
console.log(`✓ Installed hooks and MCP proxy to ${SUPERMEMORY_HOOKS_DIR}`);
console.log(`✓ Installed the supermemory-status skill to ${CODEX_SKILLS_DIR}`);

// Merge config.toml (hosted MCP server)
mergeConfigToml(true);
// Install the persistent TUI mark without overwriting an existing pet.
const petInstalled = installPetAssets();

// Merge config.toml (hosted MCP server + persistent mark)
const persistentIndicatorEnabled = mergeConfigToml(true, petInstalled);
console.log(`✓ Registered the Supermemory MCP server in ${CODEX_CONFIG_TOML}`);
if (persistentIndicatorEnabled) {
console.log("✓ Enabled the persistent Supermemory mark at the bottom of Codex");
} else if (petInstalled) {
console.log("✓ Installed the Supermemory mark and preserved your existing Codex pet selection");
}

// Merge hooks.json
mergeHooksJson(true);
Expand All @@ -438,6 +505,7 @@ You now have:
• Automatic session and prompt recall (${getRecallModeSummary()})
• Hosted Supermemory MCP tools for deeper search and explicit memory operations
• The supermemory-status skill for connection diagnostics
• A persistent Supermemory mark in compatible Codex terminals${persistentIndicatorEnabled ? "" : " (existing pet selection preserved)"}

${hadExistingConfig
? "Existing recall/capture preferences were preserved in ~/.codex/supermemory.json.\nSet recallMode to direct, off, or advisory to change recall behavior.\n"
Expand All @@ -463,9 +531,13 @@ function uninstall() {
mergeHooksJson(false);
console.log(`✓ Removed hooks from ${CODEX_HOOKS_JSON}`);

mergeConfigToml(false);
const petOwned = ownsSupermemoryPet();
mergeConfigToml(false, petOwned);
console.log(`✓ Removed the Supermemory MCP server from ${CODEX_CONFIG_TOML}`);

removePetAssets();
if (petOwned) console.log(`✓ Removed the persistent Supermemory mark from ${SUPERMEMORY_PET_DIR}`);

if (existsSync(SUPERMEMORY_HOOKS_DIR)) {
rmSync(SUPERMEMORY_HOOKS_DIR, { recursive: true, force: true });
console.log(`✓ Removed ${SUPERMEMORY_HOOKS_DIR}`);
Expand Down Expand Up @@ -539,6 +611,7 @@ function status() {
);

let mcpInstalled = false;
let persistentIndicatorEnabled = false;
if (configTomlExists) {
try {
const config = readConfigToml();
Expand All @@ -548,6 +621,9 @@ function status() {
Array.isArray(server.args) &&
server.args.length === 1 &&
server.args[0] === MCP_PROXY_SCRIPT;
persistentIndicatorEnabled =
(config.tui as Record<string, unknown> | undefined)?.pet === SUPERMEMORY_PET_ID &&
ownsSupermemoryPet();
} catch {}
}

Expand All @@ -558,6 +634,7 @@ function status() {
console.log(` hooks.json: ${hooksEnabled ? "✓ registered (implicit memory)" : "✗ not registered"}`);
console.log(` MCP server: ${mcpInstalled ? "✓ registered (hosted tools via local proxy)" : "✗ not registered"}`);
console.log(` Status skill: ${statusSkillInstalled ? "✓ installed" : "✗ not installed"}`);
console.log(` Persistent mark: ${persistentIndicatorEnabled ? "✓ enabled" : ownsSupermemoryPet() ? "○ installed, another pet selection is active" : "✗ not installed"}`);
console.log(` config.toml: ${configTomlExists ? "✓ exists" : "✗ not found"}`);

if (!apiKey || !hooksInstalled || !hooksEnabled || !mcpInstalled || !statusSkillInstalled) {
Expand Down
Loading