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
6 changes: 3 additions & 3 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ const sharedConfig = {

const executableEntries = [
{ in: "src/cli.ts", out: "dist/cli.js" },
...["recall", "flush"].map((n) => ({
...["recall", "flush", "session-start"].map((n) => ({
in: `src/hooks/${n}.ts`,
out: `dist/hooks/${n}.js`,
})),
...["search-memory", "save-memory", "forget-memory", "login"].map((n) => ({
...["search-memory", "save-memory", "forget-memory", "profile-memory", "login"].map((n) => ({
in: `src/skills/${n}.ts`,
out: `dist/skills/${n}.js`,
})),
Expand Down Expand Up @@ -48,7 +48,7 @@ await Promise.all(
);

// Copy SKILL.md files to dist
for (const skillName of ["supermemory-search", "supermemory-save", "supermemory-forget", "supermemory-login"]) {
for (const skillName of ["supermemory-search", "supermemory-save", "supermemory-forget", "supermemory-profile", "supermemory-login"]) {
mkdirSync(`dist/skills/${skillName}`, { recursive: true });
copyFileSync(
`src/skills/${skillName}/SKILL.md`,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-supermemory",
"version": "1.0.6",
"version": "1.0.7",
"description": "Persistent memory for OpenAI Codex CLI — powered by Supermemory",
"type": "module",
"main": "dist/cli.js",
Expand Down
47 changes: 41 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
rmSync,
} from "node:fs";
import { loadCredentials } from "./services/auth.js";
import { writeInstallDefaults, CONFIG_FILE, getRecallModeSummary, CONFIG } from "./config.js";
import { join, dirname } from "node:path";
import { homedir } from "node:os";
import { fileURLToPath } from "node:url";
Expand All @@ -33,15 +34,18 @@ const CODEX_HOOKS_JSON = join(CODEX_DIR, "hooks.json");
const SUPERMEMORY_HOOKS_DIR = join(CODEX_DIR, "supermemory");
const RECALL_SCRIPT = join(SUPERMEMORY_HOOKS_DIR, "recall.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 RECALL_TIMEOUT_SECONDS = 90;
const FLUSH_TIMEOUT_SECONDS = 60;
const SESSION_START_TIMEOUT_SECONDS = 60;

// Skill metadata — single source of truth for install/uninstall/status.
const SKILLS = [
{ name: "supermemory-search", script: "search-memory.js" },
{ name: "supermemory-save", script: "save-memory.js" },
{ name: "supermemory-forget", script: "forget-memory.js" },
{ name: "supermemory-profile", script: "profile-memory.js" },
{ name: "supermemory-login", script: "login.js" },
] as const;

Expand Down Expand Up @@ -193,9 +197,18 @@ function mergeHooksJson(add: boolean) {
if (add) {
const recallCmd = `node ${RECALL_SCRIPT}`;
const flushCmd = `node ${FLUSH_SCRIPT}`;
const sessionStartCmd = `node ${SESSION_START_SCRIPT}`;
const oldCaptureCmd = `node ${join(SUPERMEMORY_HOOKS_DIR, "capture.js")}`;

// Register UserPromptSubmit hook for recall
if (!hooks.SessionStart) hooks.SessionStart = [];
ensureHookRegistered(
hooks.SessionStart,
sessionStartCmd,
SESSION_START_TIMEOUT_SECONDS,
"Loading memory profile...",
);

// Register UserPromptSubmit hook for optional per-prompt recall / turn capture
if (!hooks.UserPromptSubmit) hooks.UserPromptSubmit = [];
ensureHookRegistered(hooks.UserPromptSubmit, recallCmd, RECALL_TIMEOUT_SECONDS, "Searching memories...");

Expand All @@ -212,8 +225,13 @@ function mergeHooksJson(add: boolean) {
// Remove our hooks from every MatcherGroup, then drop empty groups.
const recallCmd = `node ${RECALL_SCRIPT}`;
const flushCmd = `node ${FLUSH_SCRIPT}`;
const sessionStartCmd = `node ${SESSION_START_SCRIPT}`;
const oldCaptureCmd = `node ${join(SUPERMEMORY_HOOKS_DIR, "capture.js")}`;

if (hooks.SessionStart) {
hooks.SessionStart = removeHookCommands(hooks.SessionStart, [sessionStartCmd]);
if (hooks.SessionStart.length === 0) delete hooks.SessionStart;
}
if (hooks.UserPromptSubmit) {
hooks.UserPromptSubmit = removeHookCommands(hooks.UserPromptSubmit, [recallCmd]);
if (hooks.UserPromptSubmit.length === 0) delete hooks.UserPromptSubmit;
Expand All @@ -232,17 +250,22 @@ function install() {

ensureCodexDir();

const hadExistingConfig = existsSync(CONFIG_FILE);
writeInstallDefaults(hadExistingConfig);

// Copy hook scripts
const recallSrc = join(DIST_HOOKS_DIR, "recall.js");
const flushSrc = join(DIST_HOOKS_DIR, "flush.js");
const sessionStartSrc = join(DIST_HOOKS_DIR, "session-start.js");

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

copyFileSync(recallSrc, RECALL_SCRIPT);
copyFileSync(flushSrc, FLUSH_SCRIPT);
copyFileSync(sessionStartSrc, SESSION_START_SCRIPT);

// Remove old capture.js if it exists
const oldCapture = join(SUPERMEMORY_HOOKS_DIR, "capture.js");
Expand Down Expand Up @@ -278,8 +301,12 @@ function install() {
Installation complete!

You now have:
• Implicit memory — auto-recall on every prompt, incremental capture + final flush on session end
• Explicit memory — supermemory-search, supermemory-save, supermemory-forget, and supermemory-login skills
• Session-start profile recall (${getRecallModeSummary()})
• Explicit memory — supermemory-search, supermemory-save, supermemory-forget, supermemory-profile, supermemory-login

${hadExistingConfig
? "Existing install: legacy per-prompt recall/capture preserved in ~/.codex/supermemory.json.\nTo opt into new defaults, set autoRecallEveryPrompt=false and captureEveryNTurns=0.\n"
: "Fresh install: session-start profile + session-end flush only.\nEnable autoRecallEveryPrompt or captureEveryNTurns in ~/.codex/supermemory.json if needed.\n"}

Next steps:
1. Start Codex — on your first prompt, a browser window will open to
Expand Down Expand Up @@ -332,7 +359,10 @@ function status() {
? "credentials file (~/.codex/supermemory/credentials.json)"
: null;

const hooksInstalled = existsSync(RECALL_SCRIPT) && existsSync(FLUSH_SCRIPT);
const hooksInstalled =
existsSync(RECALL_SCRIPT) &&
existsSync(FLUSH_SCRIPT) &&
existsSync(SESSION_START_SCRIPT);
const hooksJsonExists = existsSync(CODEX_HOOKS_JSON);
const configTomlExists = existsSync(CODEX_CONFIG_TOML);

Expand All @@ -342,13 +372,17 @@ function status() {
const hooks = normalizeHookEvents(JSON.parse(readFileSync(CODEX_HOOKS_JSON, "utf-8")));
const recallCmd = `node ${RECALL_SCRIPT}`;
const flushCmd = `node ${FLUSH_SCRIPT}`;
const sessionStartCmd = `node ${SESSION_START_SCRIPT}`;
const recallRegistered = hooks.UserPromptSubmit?.some((g: MatcherGroup) =>
g.hooks.some((h: HookEntry) => h.command === recallCmd)
);
const flushRegistered = hooks.Stop?.some((g: MatcherGroup) =>
g.hooks.some((h: HookEntry) => h.command === flushCmd)
);
hooksEnabled = !!(recallRegistered && flushRegistered);
const sessionStartRegistered = hooks.SessionStart?.some((g: MatcherGroup) =>
g.hooks.some((h: HookEntry) => h.command === sessionStartCmd)
);
hooksEnabled = !!(recallRegistered && flushRegistered && sessionStartRegistered);
} catch {
// ignore
}
Expand All @@ -360,6 +394,7 @@ function status() {

console.log("codex-supermemory status:\n");
console.log(` API key: ${apiKey ? `✓ set (${apiKeySource})` : "✗ not set"}`);
console.log(` Recall mode: ${getRecallModeSummary()}`);
console.log(` Hook scripts: ${hooksInstalled ? `✓ installed at ${SUPERMEMORY_HOOKS_DIR}` : "✗ not installed"}`);
console.log(` hooks.json: ${hooksEnabled ? "✓ registered (implicit memory)" : "✗ not registered"}`);
console.log(` Skills: ${skillsInstalled ? `✓ installed (${SKILLS.map(s => s.name).join(", ")})` : "✗ not installed"}`);
Expand Down
124 changes: 65 additions & 59 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { existsSync, readFileSync } from "node:fs";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { loadCredentials } from "./services/auth.js";

const CONFIG_FILE = join(homedir(), ".codex", "supermemory.json");
export const CONFIG_FILE = join(homedir(), ".codex", "supermemory.json");
export const PLUGIN_VERSION = "1.0.7";

export interface CustomContainer {
tag: string;
Expand All @@ -21,63 +22,26 @@ interface CodexSupermemoryConfig {
projectContainerTag?: string;
filterPrompt?: string;
debug?: boolean;
// Signal extraction settings
signalExtraction?: boolean;
signalKeywords?: string[];
signalTurnsBefore?: number;
// Auto-save interval
/** @deprecated Use captureEveryNTurns */
autoSaveEveryTurns?: number;
// Custom container routing
autoRecallEveryPrompt?: boolean;
captureEveryNTurns?: number;
enableCustomContainers?: boolean;
customContainers?: CustomContainer[];
customContainerInstructions?: string;
}

const DEFAULT_SIGNAL_KEYWORDS = [
// Preferences (single words to match "i really like", "i always prefer", etc.)
"prefer",
"like",
"love",
"use",
"hate",
"dislike",
"avoid",
// Memory commands
"remember",
"forget",
"note",
// Decisions & Architecture
"decision",
"decided",
"chose",
"choose",
"picked",
"switched",
"moved",
"migrated",
"architecture",
"pattern",
"approach",
"design",
"tradeoff",
// Technical
"implementation",
"refactor",
"upgrade",
"deprecate",
// Problem solving
"bug",
"fix",
"fixed",
"solved",
"solution",
"important",
// Stack/tools
"stack",
"framework",
"library",
"tool",
"database",
"prefer", "like", "love", "use", "hate", "dislike", "avoid",
"remember", "forget", "note",
"decision", "decided", "chose", "choose", "picked", "switched", "moved", "migrated",
"architecture", "pattern", "approach", "design", "tradeoff",
"implementation", "refactor", "upgrade", "deprecate",
"bug", "fix", "fixed", "solved", "solution", "important",
"stack", "framework", "library", "tool", "database",
];

const DEFAULTS = {
Expand All @@ -89,27 +53,40 @@ const DEFAULTS = {
filterPrompt:
"You are a stateful coding agent. Remember all the information, including but not limited to user's coding preferences, tech stack, behaviours, workflows, and any other relevant details.",
debug: false,
// Signal extraction - disabled by default, captures everything
signalExtraction: false,
signalKeywords: DEFAULT_SIGNAL_KEYWORDS,
signalTurnsBefore: 3,
// Auto-save interval
autoSaveEveryTurns: 3,
autoRecallEveryPrompt: false,
captureEveryNTurns: 0,
};

function loadConfig(): CodexSupermemoryConfig {
function loadRawConfig(): { config: CodexSupermemoryConfig; existed: boolean } {
if (existsSync(CONFIG_FILE)) {
try {
const content = readFileSync(CONFIG_FILE, "utf-8");
return JSON.parse(content) as CodexSupermemoryConfig;
return { config: JSON.parse(content) as CodexSupermemoryConfig, existed: true };
} catch {
// Invalid config, use defaults
return { config: {}, existed: true };
}
}
return {};
return { config: {}, existed: false };
}

const fileConfig = loadConfig();
const { config: fileConfig, existed: configExisted } = loadRawConfig();

function resolveCaptureEveryNTurns(config: CodexSupermemoryConfig): number {
if (config.captureEveryNTurns !== undefined) return config.captureEveryNTurns;
if (config.autoSaveEveryTurns !== undefined) return config.autoSaveEveryTurns;
if (configExisted) return 3;
return DEFAULTS.captureEveryNTurns;
}

function resolveAutoRecallEveryPrompt(config: CodexSupermemoryConfig): boolean {
if (config.autoRecallEveryPrompt !== undefined) return config.autoRecallEveryPrompt;
if (configExisted) return true;
return DEFAULTS.autoRecallEveryPrompt;
}

function getApiKey(): string | undefined {
if (process.env.SUPERMEMORY_CODEX_API_KEY) return process.env.SUPERMEMORY_CODEX_API_KEY;
Expand All @@ -133,13 +110,12 @@ export const CONFIG = {
projectContainerTag: fileConfig.projectContainerTag,
filterPrompt: fileConfig.filterPrompt ?? DEFAULTS.filterPrompt,
debug: fileConfig.debug ?? DEFAULTS.debug,
// Signal extraction
signalExtraction: fileConfig.signalExtraction ?? DEFAULTS.signalExtraction,
signalKeywords: fileConfig.signalKeywords ?? DEFAULTS.signalKeywords,
signalTurnsBefore: fileConfig.signalTurnsBefore ?? DEFAULTS.signalTurnsBefore,
// Auto-save interval
autoSaveEveryTurns: fileConfig.autoSaveEveryTurns ?? DEFAULTS.autoSaveEveryTurns,
// Custom container routing
autoRecallEveryPrompt: resolveAutoRecallEveryPrompt(fileConfig),
captureEveryNTurns: resolveCaptureEveryNTurns(fileConfig),
enableCustomContainers: fileConfig.enableCustomContainers ?? false,
customContainers: (fileConfig.customContainers ?? []).filter(
(c): c is CustomContainer =>
Expand Down Expand Up @@ -213,3 +189,33 @@ export function validateContainerTag(tag: string): string | null {
const validList = validTags.map((t) => `'${t}'`).join(", ");
return `Unknown container tag '${tag}'. Valid containers: ${validList}`;
}

/** Persist explicit recall/capture defaults for fresh installs or legacy upgrades. */
export function writeInstallDefaults(isExistingInstall: boolean): void {
const current = loadRawConfig().config;
const next: CodexSupermemoryConfig = { ...current };

if (isExistingInstall) {
if (next.autoRecallEveryPrompt === undefined) {
next.autoRecallEveryPrompt = true;
}
if (next.captureEveryNTurns === undefined) {
next.captureEveryNTurns = next.autoSaveEveryTurns ?? 3;
}
} else {
next.autoRecallEveryPrompt = false;
next.captureEveryNTurns = 0;
}

writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2));
}

export function getRecallModeSummary(): string {
if (CONFIG.autoRecallEveryPrompt) {
return "legacy: recall on every prompt";
}
if (CONFIG.captureEveryNTurns > 0) {
return `unified: session-start profile + capture every ${CONFIG.captureEveryNTurns} turns + session-end flush`;
}
return "unified: session-start profile + session-end flush only";
}
Loading