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
68 changes: 61 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ and the lessons learned across every project — automatically.
`UserPromptSubmit` hook.
- 💾 **Automatic capture** — conversations are stored incrementally (every N turns) and
at session end via the `Stop` hook.
- 🏷️ **Project + user scoping** — memories are tagged per-project and per-user so
context never leaks across repos.
- 📦 **Custom container tags** — define custom memory containers (e.g., `work`, `personal`,
`code_style`). The AI automatically picks the right container based on your instructions
when saving, searching, or forgetting memories.
- 🏷️ **Project + user scoping** — automatic session memories are stored per-user,
while explicit project knowledge is tagged per-repo so context never leaks across repos.
- 🔒 **Privacy-aware** — anything wrapped in `<private>...</private>` is redacted
Expand Down Expand Up @@ -94,6 +99,9 @@ Drop this file in to override defaults:
| `signalExtraction` | `boolean` | `false` | Enable signal-based filtering (only capture turns with keywords like "prefer", "decided"). |
| `signalKeywords` | `string[]` | (defaults) | Keywords that trigger signal extraction. |
| `signalTurnsBefore` | `number` | `3` | Include N turns before a signal for context. |
| `enableCustomContainers` | `boolean` | `false` | Enable AI-driven routing to custom containers. |
| `customContainers` | `array` | `[]` | Custom containers with `tag` and `description` (see below). |
| `customContainerInstructions` | `string` | `""` | Free-text instructions for the AI on how to route memories to containers.

User tags are auto-derived from your `git config user.email`. Project tags are
derived from the Git common directory when available, so linked worktrees and
Expand All @@ -116,17 +124,63 @@ npx codex-supermemory status # show current install status

## Skills (fallback commands)

These Codex skills are available as explicit commands when you need more control:
These Codex skills are available as explicit commands when you need more control.
All memory skills support `--container <tag>` to target a specific custom container.

| Skill | Usage | Description |
| ---------------------- | ------------------------------------------ | ---------------------------------------- |
| `/supermemory-search` | `/supermemory-search <query>` | Search memories manually. |
| `/supermemory-save` | `/supermemory-save <content>` | Save a specific memory explicitly. |
| `/supermemory-forget` | `/supermemory-forget <content>` | Remove a memory. |
| `/supermemory-login` | `/supermemory-login` | Re-authenticate with Supermemory. |
| Skill | Usage | Description |
| ---------------------- | ----------------------------------------------------------- | ---------------------------------------- |
| `/supermemory-search` | `/supermemory-search [--container <tag>] <query>` | Search memories manually. |
| `/supermemory-save` | `/supermemory-save [--container <tag>] <content>` | Save a specific memory explicitly. |
| `/supermemory-forget` | `/supermemory-forget [--container <tag>] <content>` | Remove a memory. |
| `/supermemory-login` | `/supermemory-login` | Re-authenticate with Supermemory. |

Skills are fallback commands — the hooks handle most use cases automatically.

## Custom Container Tags

Custom container tags let you organize memories into separate buckets (e.g., `work`,
`personal`, `code_style`). The AI reads the container descriptions from your config
and automatically picks the right container when saving memories.

### Setup

Add these fields to `~/.codex/supermemory.json`:

```json
{
"enableCustomContainers": true,
"customContainers": [
{ "tag": "personal", "description": "Personal life — family, health, hobbies, routines" },
{ "tag": "work", "description": "Work-related — projects, deadlines, meetings, colleagues" },
{ "tag": "code_style", "description": "Coding preferences — languages, tools, patterns, conventions" }
],
"customContainerInstructions": "Route coding preferences to code_style. Personal topics to personal. Default to project container for ambiguous content."
}
```

### How it works

1. You define containers with a `tag` (identifier) and a `description` (plain English
explaining what belongs there).
2. On every prompt, the container catalog is injected into the AI's context so it knows
what containers are available.
3. When the AI saves a memory (via `/supermemory-save`), it picks the best matching
container based on the descriptions and uses `--container <tag>`.
4. When searching or forgetting, the AI can also target specific containers.
5. Automatic capture (background saving) always goes to the default project/user
containers — only explicit saves get routed to custom containers.

Each container tag automatically becomes a **Space** on the
[Supermemory dashboard](https://app.supermemory.ai), so you can view and manage
memories organized by category.

### Container config reference

| Field | Type | Description |
| ------------------ | -------- | -------------------------------------------------- |
| `tag` | `string` | Unique identifier for the container (e.g. `work`). |
| `description` | `string` | Plain English description for AI routing. |

## Privacy

Anything wrapped in `<private>...</private>` is replaced with `[REDACTED]` before
Expand Down
62 changes: 62 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { loadCredentials } from "./services/auth.js";

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

export interface CustomContainer {
tag: string;
description: string;
}

interface CodexSupermemoryConfig {
apiKey?: string;
similarityThreshold?: number;
Expand All @@ -22,6 +27,10 @@ interface CodexSupermemoryConfig {
signalTurnsBefore?: number;
// Auto-save interval
autoSaveEveryTurns?: number;
// Custom container routing
enableCustomContainers?: boolean;
customContainers?: CustomContainer[];
customContainerInstructions?: string;
}

const DEFAULT_SIGNAL_KEYWORDS = [
Expand Down Expand Up @@ -130,6 +139,13 @@ export const CONFIG = {
signalTurnsBefore: fileConfig.signalTurnsBefore ?? DEFAULTS.signalTurnsBefore,
// Auto-save interval
autoSaveEveryTurns: fileConfig.autoSaveEveryTurns ?? DEFAULTS.autoSaveEveryTurns,
// Custom container routing
enableCustomContainers: fileConfig.enableCustomContainers ?? false,
customContainers: (fileConfig.customContainers ?? []).filter(
(c): c is CustomContainer =>
!!c && typeof c.tag === "string" && typeof c.description === "string",
),
customContainerInstructions: fileConfig.customContainerInstructions ?? "",
};

export function isConfigured(): boolean {
Expand All @@ -151,3 +167,49 @@ export function getSignalConfig(): {
turnsBefore: CONFIG.signalTurnsBefore,
};
}

export function getContainerCatalog(): string | null {
if (!CONFIG.enableCustomContainers || CONFIG.customContainers.length === 0) {
return null;
}

const lines: string[] = [];
lines.push("Custom memory containers are available for organizing memories:");
lines.push("");
for (const c of CONFIG.customContainers) {
lines.push(`- \`${c.tag}\`: ${c.description}`);
}

if (CONFIG.customContainerInstructions) {
lines.push("");
lines.push(CONFIG.customContainerInstructions);
}

lines.push("");
lines.push(
"When saving memories with /supermemory-save, use --container <tag> to route to a specific container.",
);
lines.push(
"When searching with /supermemory-search, use --container <tag> to search a specific container.",
);
lines.push(
"When forgetting with /supermemory-forget, use --container <tag> to target a specific container.",
);
lines.push("If no container is specified, memories go to the default project/user containers.");

return lines.join("\n");
}

export function validateContainerTag(tag: string): string | null {
if (!CONFIG.enableCustomContainers || CONFIG.customContainers.length === 0) {
return "Custom containers are not enabled. Remove --container or set enableCustomContainers in config.";
}

const validTags = CONFIG.customContainers.map((c) => c.tag);
if (validTags.includes(tag)) {
return null;
}

const validList = validTags.map((t) => `'${t}'`).join(", ");
return `Unknown container tag '${tag}'. Valid containers: ${validList}`;
}
21 changes: 19 additions & 2 deletions src/hooks/recall.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { readFileSync, existsSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { homedir } from "node:os";
import { isConfigured, CONFIG, reloadApiKey } from "../config.js";
import { isConfigured, CONFIG, reloadApiKey, getContainerCatalog } from "../config.js";
import { SupermemoryClient } from "../services/client.js";
import { getTags } from "../services/tags.js";
import { formatCombinedContext } from "../services/context.js";
Expand Down Expand Up @@ -140,9 +140,26 @@ async function main() {
seenCount: seen.size,
});

const containerCatalog = getContainerCatalog();

if (newFacts.length > 0) {
addSeenFacts(sessionId, newFacts);
exitWithContext(`[SUPERMEMORY CONTEXT]\n${text}\n[END SUPERMEMORY CONTEXT]`);
let additionalContext = `[SUPERMEMORY CONTEXT]\n${text}\n[END SUPERMEMORY CONTEXT]`;

if (containerCatalog) {
additionalContext += `\n\n[SUPERMEMORY CONTAINERS]\n${containerCatalog}\n[END SUPERMEMORY CONTAINERS]`;
}

log("recall: emit context", {
additionalContextLength: additionalContext.length,
});
exitWithContext(additionalContext);
} else if (containerCatalog) {
const additionalContext = `[SUPERMEMORY CONTAINERS]\n${containerCatalog}\n[END SUPERMEMORY CONTAINERS]`;
log("recall: emit container catalog only", {
additionalContextLength: additionalContext.length,
});
exitWithContext(additionalContext);
} else {
Comment thread
ved015 marked this conversation as resolved.
exitWithContext("");
}
Expand Down
7 changes: 6 additions & 1 deletion src/services/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,12 @@ export async function captureEntries(
});

const transcript = formatTranscript(signalEntries);
const content = `[Session ${sessionId}]\n${transcript}`;
const rawContent = `[Session ${sessionId}]\n${transcript}`;

const content = rawContent
.replace(/\[SUPERMEMORY CONTAINERS\][\s\S]*?\[END SUPERMEMORY CONTAINERS\]\s*/g, "")
.replace(/<supermemory-containers>[\s\S]*?<\/supermemory-containers>\s*/g, "")
Comment thread
ved015 marked this conversation as resolved.
.trim();

const metadata = {
type: "conversation" as const,
Expand Down
6 changes: 5 additions & 1 deletion src/services/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,11 @@ export class SupermemoryClient {
metadata?: { type?: MemoryType; tool?: string; [key: string]: unknown },
options?: { customId?: string }
) {
log("addMemory: start", { containerTag, contentLength: content.length, customId: options?.customId });
log("addMemory: start", {
containerTag,
contentLength: content.length,
customId: options?.customId,
});
try {
const payload: {
content: string;
Expand Down
84 changes: 58 additions & 26 deletions src/skills/forget-memory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import { isConfigured } from "../config.js";
import { isConfigured, validateContainerTag } from "../config.js";
import { SupermemoryClient } from "../services/client.js";
import { getProjectTag, getUserTag } from "../services/tags.js";

function parseArgs(args: string[]): { content: string; containerTag?: string } {
let containerTag: string | undefined;
const contentParts: string[] = [];

for (let i = 0; i < args.length; i++) {
if (args[i] === "--container" && i + 1 < args.length) {
containerTag = args[++i];
} else {
contentParts.push(args[i]);
}
}

return { content: contentParts.join(" "), containerTag };
}

async function main(): Promise<void> {
if (!isConfigured()) {
console.error(
Expand All @@ -11,45 +26,62 @@ async function main(): Promise<void> {
process.exit(1);
}

const content = process.argv.slice(2).join(" ");
const { content, containerTag } = parseArgs(process.argv.slice(2));

if (!content.trim()) {
console.log(
'No content provided. Usage: node forget-memory.js "content to forget"'
'No content provided. Usage: node forget-memory.js [--container <tag>] "content to forget"'
);
process.exit(0);
}

const client = new SupermemoryClient();
const projectTag = getProjectTag(process.cwd());
const userTag = getUserTag();

if (containerTag) {
const validationError = validateContainerTag(containerTag);
if (validationError) {
console.log(validationError);
process.exit(1);
}
}

try {
// Forget from both project and user scopes since memories may exist in either.
const [projectResult, userResult] = await Promise.all([
client.forgetMemory(content, projectTag),
client.forgetMemory(content, userTag),
]);
if (containerTag) {
const result = await client.forgetMemory(content, containerTag);
if (result.success) {
console.log(`Memory forgotten from container '${containerTag}'${result.id ? ` (id: ${result.id})` : ""}`);
} else {
console.log(`Failed to forget memory from container '${containerTag}': ${result.error}`);
}
} else {
const projectTag = getProjectTag(process.cwd());
const userTag = getUserTag();

const forgotten: string[] = [];
const errors: string[] = [];
const [projectResult, userResult] = await Promise.all([
client.forgetMemory(content, projectTag),
client.forgetMemory(content, userTag),
]);

if (projectResult.success) {
forgotten.push(projectResult.id ? `project (id: ${projectResult.id})` : "project");
} else {
errors.push(`project: ${projectResult.error}`);
}
const forgotten: string[] = [];
const errors: string[] = [];

if (userResult.success) {
forgotten.push(userResult.id ? `user (id: ${userResult.id})` : "user");
} else {
errors.push(`user: ${userResult.error}`);
}
if (projectResult.success) {
forgotten.push(projectResult.id ? `project (id: ${projectResult.id})` : "project");
} else {
errors.push(`project: ${projectResult.error}`);
}

if (forgotten.length > 0) {
console.log(`Memory forgotten from: ${forgotten.join(", ")}`);
} else {
console.log(`Failed to forget memory: ${errors.join("; ")}`);
if (userResult.success) {
forgotten.push(userResult.id ? `user (id: ${userResult.id})` : "user");
} else {
errors.push(`user: ${userResult.error}`);
}

if (forgotten.length > 0) {
console.log(`Memory forgotten from: ${forgotten.join(", ")}`);
} else {
console.log(`Failed to forget memory: ${errors.join("; ")}`);
}
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
Expand Down
Loading