From a5540af7959d785c0e105d7bca953ccc44f9b3d6 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Tue, 25 Aug 2026 20:31:15 +0530 Subject: [PATCH] Add persistent Supermemory CLI mark --- README.md | 7 ++ build.mjs | 168 +++++++++++++++++++++++++++++++++++++++++++++++ src/cli.ts | 91 +++++++++++++++++++++++-- src/pet/pet.json | 12 ++++ test/unit.mjs | 12 +++- 5 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 src/pet/pet.json diff --git a/README.md b/README.md index 61172f6..f0cb49b 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/build.mjs b/build.mjs index 54ea048..027677e 100644 --- a/build.mjs +++ b/build.mjs @@ -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") @@ -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 }); diff --git a/src/cli.ts b/src/cli.ts index 1f77969..3cf131e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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; @@ -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); @@ -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 @@ -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; + 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 | undefined; const server = mcpServers?.supermemory as Record | undefined; @@ -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 | 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 { @@ -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); } @@ -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); @@ -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" @@ -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}`); @@ -539,6 +611,7 @@ function status() { ); let mcpInstalled = false; + let persistentIndicatorEnabled = false; if (configTomlExists) { try { const config = readConfigToml(); @@ -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 | undefined)?.pet === SUPERMEMORY_PET_ID && + ownsSupermemoryPet(); } catch {} } @@ -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) { diff --git a/src/pet/pet.json b/src/pet/pet.json new file mode 100644 index 0000000..c05a92d --- /dev/null +++ b/src/pet/pet.json @@ -0,0 +1,12 @@ +{ + "id": "supermemory", + "displayName": "Supermemory", + "description": "A quiet persistent indicator that Supermemory is installed", + "spritesheetPath": "spritesheet.png", + "frame": { + "width": 192, + "height": 208, + "columns": 8, + "rows": 9 + } +} diff --git a/test/unit.mjs b/test/unit.mjs index 11cb618..f870735 100644 --- a/test/unit.mjs +++ b/test/unit.mjs @@ -802,6 +802,10 @@ describe("integration: install/uninstall", () => { assert.deepEqual(toml.mcp_servers.supermemory.args, [ join(codexDir, "supermemory", "mcp-proxy.js"), ]); + assert.equal(toml.tui.pet, "supermemory"); + assert.equal(toml.tui.pet_anchor, "screen-bottom"); + assert.ok(existsSync(join(codexDir, "pets", "supermemory", "pet.json"))); + assert.ok(existsSync(join(codexDir, "pets", "supermemory", "spritesheet.png"))); const config = JSON.parse(readFileSync(join(codexDir, "supermemory.json"), "utf-8")); assert.equal(config.recallMode, "direct"); assert.equal(config.captureEveryNTurns, 0); @@ -857,6 +861,7 @@ describe("integration: install/uninstall", () => { const skillsDir = join(codexDir, "skills"); assert.ok(!existsSync(join(skillsDir, "supermemory-status"))); + assert.ok(!existsSync(join(codexDir, "pets", "supermemory"))); }); test("uninstall drops empty [features] section", (t) => { @@ -948,7 +953,10 @@ describe("integration: install/uninstall", () => { test("install merges into existing valid config.toml", (t) => { const { tmpDir, configPath } = setupCodexHome(t); - writeFileSync(configPath, 'model = "gpt-5"\n\n[features]\nweb_search = true\n'); + writeFileSync( + configPath, + 'model = "gpt-5"\n\n[features]\nweb_search = true\n\n[tui]\npet = "dewey"\npet_anchor = "composer"\n', + ); const result = runCli(cliBin, "install", tmpDir); @@ -958,6 +966,8 @@ describe("integration: install/uninstall", () => { assert.equal(config.features.web_search, true); assert.equal(config.features.codex_hooks, undefined); assert.equal(config.mcp_servers.supermemory.command, "node"); + assert.equal(config.tui.pet, "dewey"); + assert.equal(config.tui.pet_anchor, "composer"); }); });