From 9f94787e8a4015d3b2c710df329f0e916f886b22 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Fri, 22 May 2026 13:57:47 -0700 Subject: [PATCH 1/2] feat: session-start recall, source attribution, and install defaults Add SessionStart profile hook, sm_source/x-sm-source for analytics, safer fresh-install defaults with legacy migration, and supermemory-profile skill. Bump to 1.0.7. --- build.mjs | 6 +- package-lock.json | 4 +- package.json | 2 +- src/cli.ts | 47 +++++++-- src/config.ts | 124 +++++++++++++----------- src/hooks/recall.ts | 35 +++---- src/hooks/session-start.ts | 117 ++++++++++++++++++++++ src/services/capture.ts | 1 + src/services/client.ts | 20 +++- src/skills/profile-memory.ts | 46 +++++++++ src/skills/supermemory-profile/SKILL.md | 14 +++ 11 files changed, 323 insertions(+), 93 deletions(-) create mode 100644 src/hooks/session-start.ts create mode 100644 src/skills/profile-memory.ts create mode 100644 src/skills/supermemory-profile/SKILL.md diff --git a/build.mjs b/build.mjs index 2198942..a3477dc 100644 --- a/build.mjs +++ b/build.mjs @@ -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`, })), @@ -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`, diff --git a/package-lock.json b/package-lock.json index 04fff45..648d894 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codex-supermemory", - "version": "1.0.6", + "version": "1.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codex-supermemory", - "version": "1.0.6", + "version": "1.0.7", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 1ed8896..b6075b9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/cli.ts b/src/cli.ts index ba1949c..e01450f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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"; @@ -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; @@ -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..."); @@ -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; @@ -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"); @@ -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 @@ -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); @@ -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 } @@ -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"}`); diff --git a/src/config.ts b/src/config.ts index 25be6ed..dfcf5d6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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; @@ -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 = { @@ -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; @@ -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 => @@ -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"; +} diff --git a/src/hooks/recall.ts b/src/hooks/recall.ts index a248960..48bc154 100644 --- a/src/hooks/recall.ts +++ b/src/hooks/recall.ts @@ -22,9 +22,6 @@ interface CodexHookPayload { [key: string]: unknown; } -// Output shape required by Codex UserPromptSubmitCommandOutputWire. -// Empty context is emitted as a silent exit so Codex doesn't render a -// "hook context:" label with no content. function exitWithContext(additionalContext: string): never { if (additionalContext) { process.stdout.write( @@ -40,7 +37,6 @@ function exitWithContext(additionalContext: string): never { } async function main() { - // Read stdin via fd 0 let rawInput = ""; try { rawInput = readFileSync(0, "utf-8"); @@ -99,32 +95,33 @@ async function main() { const tags = getTags(cwd); const sessionId = getSessionId(payload.session_id, tags.project); - log("recall: start", { query: query.slice(0, 100), tags, sessionId }); + log("recall: start", { + query: query.slice(0, 100), + tags, + sessionId, + autoRecallEveryPrompt: CONFIG.autoRecallEveryPrompt, + }); - // Find transcript path - either from payload or by searching const transcriptPath = resolveTranscriptPath(payload.transcript_path, sessionId); - if (transcriptPath) { - log("recall: found transcript", { sessionId, transcriptPath }); - } - const client = new SupermemoryClient(); - // Step 1: Capture any new entries from previous turns BEFORE recall - await captureEntries("recall", client, sessionId, transcriptPath, tags, { - requireMinEntries: 2, - requireMinTurns: CONFIG.autoSaveEveryTurns, - }); + if (CONFIG.captureEveryNTurns > 0) { + await captureEntries("recall", client, sessionId, transcriptPath, tags, { + requireMinEntries: 2, + requireMinTurns: CONFIG.captureEveryNTurns, + }); + } + + if (!CONFIG.autoRecallEveryPrompt) { + exitWithContext(""); + } - // Step 2: Now search for relevant memories (including what we just captured) - // Query both containers: user profile from user container, memories from project container. - // The profile() API only accepts a single containerTag, so we make parallel calls. try { const [profileResult, projectSearchResult] = await Promise.all([ client.getProfileWithSearch(tags.user, query), client.searchMemories(query, tags.project), ]); - // Get facts already shown in this session to avoid repeating them const seen = getSeenFacts(sessionId); const { text, newFacts } = formatCombinedContext( profileResult, diff --git a/src/hooks/session-start.ts b/src/hooks/session-start.ts new file mode 100644 index 0000000..12adf53 --- /dev/null +++ b/src/hooks/session-start.ts @@ -0,0 +1,117 @@ +import { readFileSync, existsSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { homedir } from "node:os"; +import { createHash } from "node:crypto"; +import { PLUGIN_VERSION } from "../config.js"; +import { SupermemoryClient } from "../services/client.js"; +import { getTags } from "../services/tags.js"; +import { formatCombinedContext } from "../services/context.js"; +import { log } from "../services/logger.js"; +import { startAuthFlow, AUTH_BASE_URL } from "../services/auth.js"; +import { getSeenFacts, addSeenFacts } from "../services/factCache.js"; + +const AUTH_ATTEMPTED_FILE = join(homedir(), ".codex", "supermemory", ".auth-attempted"); + +interface CodexHookPayload { + session_id?: string; + cwd?: string; + [key: string]: unknown; +} + +function exitWithContext(additionalContext: string): never { + if (additionalContext) { + process.stdout.write( + JSON.stringify({ + hookSpecificOutput: { + hookEventName: "SessionStart", + additionalContext, + }, + }) + ); + } + process.exit(0); +} + +function sessionHash(sessionId: string): string { + return createHash("sha256").update(sessionId).digest("hex").slice(0, 16); +} + +async function main() { + let rawInput = ""; + try { + rawInput = readFileSync(0, "utf-8"); + } catch { + process.exit(0); + } + + if (!isConfigured()) { + const alreadyAttempted = existsSync(AUTH_ATTEMPTED_FILE); + if (!alreadyAttempted) { + try { + mkdirSync(dirname(AUTH_ATTEMPTED_FILE), { recursive: true }); + writeFileSync(AUTH_ATTEMPTED_FILE, new Date().toISOString()); + } catch {} + + try { + await startAuthFlow(); + reloadApiKey(); + try { unlinkSync(AUTH_ATTEMPTED_FILE); } catch {} + } catch { + exitWithContext( + "[SUPERMEMORY] Memory is installed but NOT active — missing API key.\n" + + `Visit: ${AUTH_BASE_URL}\n` + + "Run /supermemory-login to authenticate." + ); + } + } else { + exitWithContext( + "[SUPERMEMORY] Memory is installed but NOT active — missing API key.\n" + + "Run /supermemory-login to authenticate." + ); + } + } + + let payload: CodexHookPayload = {}; + try { + payload = JSON.parse(rawInput) as CodexHookPayload; + } catch { + exitWithContext(""); + } + + const sessionId = payload.session_id || `codex_${Date.now()}`; + const cwd = payload.cwd || process.cwd(); + const tags = getTags(cwd); + const client = new SupermemoryClient(); + + log("session-start: begin", { sessionId, tags }); + + try { + const profileResult = await client.getProfile(tags.user); + const seen = getSeenFacts(sessionId); + const { text, newFacts } = formatCombinedContext( + { + success: profileResult.success, + profile: profileResult.profile, + searchResults: undefined, + }, + 0, + CONFIG.maxProfileItems, + undefined, + seen, + ); + + if (newFacts.length > 0) { + addSeenFacts(sessionId, newFacts); + exitWithContext(`[SUPERMEMORY CONTEXT]\n${text}\n[END SUPERMEMORY CONTEXT]`); + } + + exitWithContext(""); + } catch (error) { + log("session-start: error", { error: String(error) }); + exitWithContext(""); + } +} + +main().catch(() => { + exitWithContext(""); +}); diff --git a/src/services/capture.ts b/src/services/capture.ts index d5fb0c0..1d26d45 100644 --- a/src/services/capture.ts +++ b/src/services/capture.ts @@ -134,6 +134,7 @@ export async function captureEntries( sessionId, entryCount: newEntries.length, timestamp: new Date().toISOString(), + sm_capture_mode: caller === "flush" ? "session_end" : "turn", }; // Save automatic transcript capture to the user container. Explicit project diff --git a/src/services/client.ts b/src/services/client.ts index 3acc06c..85b60b2 100644 --- a/src/services/client.ts +++ b/src/services/client.ts @@ -1,5 +1,5 @@ import Supermemory from "supermemory"; -import { CONFIG, isConfigured, getApiKeyValue } from "../config.js"; +import { CONFIG, isConfigured, getApiKeyValue, PLUGIN_VERSION } from "../config.js"; import { log } from "./logger.js"; import type { MemoryType } from "../types/index.js"; @@ -64,7 +64,12 @@ export class SupermemoryClient { if (!isConfigured()) { throw new Error("SUPERMEMORY_API_KEY not set"); } - this.client = new Supermemory({ apiKey: getApiKeyValue() }); + // `x-sm-source` is read by mono's API to attribute searches and + // writes to the Codex plugin in PostHog / `document.source`. + this.client = new Supermemory({ + apiKey: getApiKeyValue(), + defaultHeaders: { "x-sm-source": "codex" }, + }); } return this.client; } @@ -187,6 +192,15 @@ export class SupermemoryClient { customId: options?.customId, }); try { + // Always stamp `sm_source` so mono's `document.source` column attributes + // these writes to the Codex plugin. Caller-provided metadata wins on + // conflicts so a tool can override the source if it ever needs to. + const mergedMetadata = { + sm_source: "codex", + sm_plugin_version: PLUGIN_VERSION, + ...(metadata ?? {}), + } as Record; + const payload: { content: string; containerTag: string; @@ -195,7 +209,7 @@ export class SupermemoryClient { } = { content, containerTag, - metadata: metadata as Record, + metadata: mergedMetadata, }; if (options?.customId) { payload.customId = options.customId; diff --git a/src/skills/profile-memory.ts b/src/skills/profile-memory.ts new file mode 100644 index 0000000..5132cd2 --- /dev/null +++ b/src/skills/profile-memory.ts @@ -0,0 +1,46 @@ +#!/usr/bin/env node +import { isConfigured } from "../config.js"; +import { SupermemoryClient } from "../services/client.js"; +import { getTags } from "../services/tags.js"; + +async function main() { + if (!isConfigured()) { + console.error("Supermemory is not authenticated. Run /supermemory-login first."); + process.exit(1); + } + + const cwd = process.cwd(); + const tags = getTags(cwd); + const client = new SupermemoryClient(); + const result = await client.getProfile(tags.user); + + if (!result.success || !result.profile) { + console.log("No profile available yet."); + process.exit(0); + } + + const staticFacts = result.profile.static ?? []; + const dynamicFacts = result.profile.dynamic ?? []; + const lines: string[] = []; + + if (staticFacts.length > 0) { + lines.push("[User Profile — Static]"); + staticFacts.forEach((fact, i) => lines.push(`${i + 1}. ${fact}`)); + } + if (dynamicFacts.length > 0) { + lines.push("[User Profile — Recent]"); + dynamicFacts.forEach((fact, i) => lines.push(`${i + 1}. ${fact}`)); + } + + if (lines.length === 0) { + console.log("Profile is empty."); + process.exit(0); + } + + console.log(lines.join("\n")); +} + +main().catch((error) => { + console.error(String(error)); + process.exit(1); +}); diff --git a/src/skills/supermemory-profile/SKILL.md b/src/skills/supermemory-profile/SKILL.md new file mode 100644 index 0000000..8fad240 --- /dev/null +++ b/src/skills/supermemory-profile/SKILL.md @@ -0,0 +1,14 @@ +--- +name: supermemory-profile +description: View the user's Supermemory profile (persistent facts and recent context). +--- + +# Supermemory Profile + +Fetch the user profile from Supermemory: + +```bash +node profile-memory.js +``` + +Use when the user asks what you remember about them or wants to inspect stored profile facts. From 32461ce7f7e01a12be310ed5b3ef1a496a0d6acf Mon Sep 17 00:00:00 2001 From: Sreeram Sreedhar Date: Sun, 24 May 2026 01:31:01 -0400 Subject: [PATCH 2/2] fix: import config helpers in session-start hook --- src/hooks/session-start.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/hooks/session-start.ts b/src/hooks/session-start.ts index 12adf53..3afce68 100644 --- a/src/hooks/session-start.ts +++ b/src/hooks/session-start.ts @@ -1,8 +1,7 @@ import { readFileSync, existsSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs"; import { join, dirname } from "node:path"; import { homedir } from "node:os"; -import { createHash } from "node:crypto"; -import { PLUGIN_VERSION } from "../config.js"; +import { isConfigured, CONFIG, reloadApiKey } from "../config.js"; import { SupermemoryClient } from "../services/client.js"; import { getTags } from "../services/tags.js"; import { formatCombinedContext } from "../services/context.js"; @@ -32,10 +31,6 @@ function exitWithContext(additionalContext: string): never { process.exit(0); } -function sessionHash(sessionId: string): string { - return createHash("sha256").update(sessionId).digest("hex").slice(0, 16); -} - async function main() { let rawInput = ""; try {