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
40 changes: 9 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,7 @@ bunx opencode-supermemory@latest install --no-tui
```

This registers the plugin in `~/.config/opencode/opencode.jsonc` for both OpenCode V1
and OpenCode 2, and creates the `/supermemory-index` command. Add
`--disable-context-recovery` if the user also has
[Oh My OpenCode](#usage-with-oh-my-opencode) installed, to avoid its auto-compact hook
fighting with this plugin's V1 compaction.
and OpenCode 2, and creates the `/supermemory-index` command.

- Register the plugin in `~/.config/opencode/opencode.jsonc` (`plugin` for V1, `plugins`
for OpenCode 2, plus an `allow` rule for the read-only `supermemory_recall` tool)
Expand Down Expand Up @@ -127,7 +124,7 @@ Run `/supermemory-index` to have the agent explore and memorize the codebase. `/
| --- | --- |
| 🧠 **Context injection**<br>On a session's first message, the agent silently receives your profile, all project knowledge, and (if `autoRecallEveryPrompt` is on) a semantic search over personal memories. | 🔎 **Reasoned recall**<br>Every turn, the agent is shown a directive asking it to decide whether recalling memory would help before answering. It searches via the `supermemory` tool only when it decides to; the search itself is auto-approved. |
| 💾 **Automatic capture**<br>Completed turns are saved every `captureEveryNTurns` turns, with any remainder flushed when the session ends or OpenCode shuts down. Synthetic plugin context is excluded and `<private>` content is redacted. | 🗣️ **Keyword detection**<br>Saying "remember", "save this", "don't forget", or a custom pattern nudges the agent to save to memory. |
| 🧭 **Codebase indexing**<br>`/supermemory-init` has the agent explore and memorize the codebase's structure, patterns, and conventions. | 🗜️ **Compaction memory**<br>On OpenCode V1, triggers summarization at 80% context capacity. On OpenCode 2, enriches the native compaction request. Both inject project memories into the summary and save the summary itself as a memory. |
| 🧭 **Codebase indexing**<br>`/supermemory-init` has the agent explore and memorize the codebase's structure, patterns, and conventions. | 🗜️ **Compaction memory**<br>When OpenCode compacts a long session, project memories are added to the summary and the summary itself is saved as a memory. OpenCode decides when to compact. |
| 🔒 **Privacy**<br>Content wrapped in `<private>...</private>` is never stored. | 🔔 **Update notices**<br>Checks npm for a newer release on session start and surfaces a one-line notice. |

```
Expand Down Expand Up @@ -186,13 +183,13 @@ Run `/supermemory-index` to explore and memorize your codebase structure, patter

### Compaction Memory

On OpenCode V1, when context hits 80% capacity (`compactionThreshold`) the plugin triggers
OpenCode's summarization, injects project memories into the summary prompt, and saves the
resulting summary as a memory.
OpenCode owns compaction on both V1 and OpenCode 2: it decides when to compact, which model
writes the summary, and how the session continues. The plugin adds bounded project memories
to OpenCode's own compaction prompt, then saves each successful summary as a memory. It never
starts a compaction or sends a continuation itself.

On OpenCode 2, OpenCode owns the compaction trigger and model. The plugin hooks the native
compaction request to add the same project memories, then saves each successful summary as
a memory. Set `compactionEnabled: false` to opt out on OpenCode 2.
Set `compactionEnabled: false` to turn this off. The older `compactionThreshold` setting is
no longer used to trigger compaction; a value of `0` or `false` still turns it off.

### Activity Notices and Status Footer

Expand Down Expand Up @@ -244,8 +241,6 @@ What differs on OpenCode 2:
- Recall runs through the read-only `supermemory_recall` tool (search only). The installer
allows it without prompting; `add`, `forget`, and the rest stay behind the normal
`supermemory` permission. An explicit `deny` for `supermemory_recall` is preserved.
- Compaction is native: the plugin enriches OpenCode's compaction request and saves the
summary instead of triggering compaction itself (`compactionEnabled`).
- Recalled context is attached to the outgoing model request for the current prompt rather
than persisted into the transcript.

Expand Down Expand Up @@ -374,10 +369,7 @@ scope, not from a different or malformed response.
// Extra keyword patterns for memory detection (regex)
"keywordPatterns": ["log\\s+this", "write\\s+down"],

// OpenCode V1: context usage ratio that triggers compaction (0-1)
"compactionThreshold": 0.8,

// OpenCode 2: enrich native compaction with project memories and save summaries
// Add project memories to OpenCode's compaction summaries and save the summaries
"compactionEnabled": true,

// Save completed conversation batches every N turns (0 = session end only)
Expand Down Expand Up @@ -418,20 +410,6 @@ This is useful to preserve a legacy personal memory container, sync memories bet
machines for the same project, organize memories with your own naming scheme, or
integrate with existing Supermemory container tags from other tools.

## Usage with Oh My OpenCode

If you're using [Oh My OpenCode](https://github.com/code-yeongyu/oh-my-opencode),
disable its built-in auto-compact hook to let supermemory handle context compaction
(or pass `--disable-context-recovery` to `install`):

```json
{
"disabled_hooks": ["anthropic-context-window-limit-recovery"]
}
```

Add that to `~/.config/opencode/oh-my-opencode.json`.

<details>
<summary>Development</summary>
<br>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"hooks": [
"chat.message",
"permission.ask",
"experimental.session.compacting",
"event"
]
},
Expand Down
84 changes: 3 additions & 81 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const OPENCODE_TUI_CONFIGS = [
join(OPENCODE_CONFIG_DIR, "tui.jsonc"),
join(OPENCODE_CONFIG_DIR, "tui.json"),
];
const OH_MY_OPENCODE_CONFIG = join(OPENCODE_CONFIG_DIR, "oh-my-opencode.json");
const DEFAULT_CONFIG_FILE = CONFIG_FILE ?? join(OPENCODE_CONFIG_DIR, "supermemory.json");

const SUPERMEMORY_INDEX_COMMAND = `---
Expand Down Expand Up @@ -359,58 +358,8 @@ function createCommands(): boolean {
return true;
}

function isOhMyOpencodeInstalled(): boolean {
const configPath = findOpencodeConfig();
if (!configPath) return false;

try {
const content = readFileSync(configPath, "utf-8");
return content.includes("oh-my-opencode");
} catch {
return false;
}
}

function isAutoCompactAlreadyDisabled(): boolean {
if (!existsSync(OH_MY_OPENCODE_CONFIG)) return false;

try {
const content = readFileSync(OH_MY_OPENCODE_CONFIG, "utf-8");
const config = JSON.parse(content);
const disabledHooks = config.disabled_hooks as string[] | undefined;
return disabledHooks?.includes("anthropic-context-window-limit-recovery") ?? false;
} catch {
return false;
}
}

function disableAutoCompactHook(): boolean {
try {
let config: Record<string, unknown> = {};

if (existsSync(OH_MY_OPENCODE_CONFIG)) {
const content = readFileSync(OH_MY_OPENCODE_CONFIG, "utf-8");
config = JSON.parse(content);
}

const disabledHooks = (config.disabled_hooks as string[]) || [];
if (!disabledHooks.includes("anthropic-context-window-limit-recovery")) {
disabledHooks.push("anthropic-context-window-limit-recovery");
}
config.disabled_hooks = disabledHooks;

writeFileSync(OH_MY_OPENCODE_CONFIG, JSON.stringify(config, null, 2));
console.log(`✓ Disabled anthropic-context-window-limit-recovery hook in oh-my-opencode.json`);
return true;
} catch (err) {
console.error("✗ Failed to update oh-my-opencode.json:", err);
return false;
}
}

interface InstallOptions {
tui: boolean;
disableAutoCompact: boolean;
}

async function install(options: InstallOptions): Promise<number> {
Expand Down Expand Up @@ -464,33 +413,9 @@ async function install(options: InstallOptions): Promise<number> {
createCommands();
}

// Step 3: Configure Oh My OpenCode (if installed)
if (isOhMyOpencodeInstalled()) {
console.log("\nStep 3: Configure Oh My OpenCode");
console.log("Detected Oh My OpenCode plugin.");
console.log("Supermemory handles context compaction, so the built-in context-window-limit-recovery hook should be disabled.");

if (isAutoCompactAlreadyDisabled()) {
console.log("✓ anthropic-context-window-limit-recovery hook already disabled");
} else {
if (options.tui) {
const shouldDisable = await confirm(rl!, "Disable anthropic-context-window-limit-recovery hook to let Supermemory handle context?");
if (!shouldDisable) {
console.log("Skipped.");
} else {
disableAutoCompactHook();
}
} else if (options.disableAutoCompact) {
disableAutoCompactHook();
} else {
console.log("Skipped. Use --disable-context-recovery to disable the hook in non-interactive mode.");
}
}
}

if (rl) rl.close();

// Step 4: Authenticate
// Final step: Authenticate
console.log("\n" + "─".repeat(50));
console.log("\n🔑 Final step: Authenticate with Supermemory\n");

Expand Down Expand Up @@ -707,7 +632,6 @@ opencode-supermemory - Persistent memory for OpenCode agents
Commands:
install Install and configure the plugin
--no-tui Non-interactive mode (for LLM agents)
--disable-context-recovery Disable Oh My OpenCode's context hook
login Authenticate with Supermemory (opens browser)
logout Clear stored credentials
status Show Supermemory connection status
Expand All @@ -729,13 +653,11 @@ if (args.length === 0 || args[0] === "help" || args[0] === "--help" || args[0] =

if (args[0] === "install") {
const noTui = args.includes("--no-tui");
const disableAutoCompact = args.includes("--disable-context-recovery");
install({ tui: !noTui, disableAutoCompact }).then((code) => process.exit(code));
install({ tui: !noTui }).then((code) => process.exit(code));
} else if (args[0] === "setup") {
console.log("Note: 'setup' is deprecated. Use 'install' instead.\n");
const noTui = args.includes("--no-tui");
const disableAutoCompact = args.includes("--disable-context-recovery");
install({ tui: !noTui, disableAutoCompact }).then((code) => process.exit(code));
install({ tui: !noTui }).then((code) => process.exit(code));
} else if (args[0] === "login") {
login().then((code) => process.exit(code));
} else if (args[0] === "logout") {
Expand Down
40 changes: 29 additions & 11 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ interface SupermemoryConfig {
projectContainerTag?: string;
filterPrompt?: string;
keywordPatterns?: string[];
compactionThreshold?: number;
/** OpenCode 2 only: enrich native compaction with project memories and save summaries. */
/** Enrich OpenCode's native compaction with project memories and save summaries. */
compactionEnabled?: boolean;
/**
* @deprecated OpenCode now decides when to compact. Only `0` or `false` is
* still read, as a way to turn compaction memory off; use compactionEnabled.
*/
compactionThreshold?: number | false;
autoRecallEveryPrompt?: boolean;
captureEveryNTurns?: number;
recallDirective?: string | null;
Expand All @@ -57,6 +61,8 @@ const DEFAULT_KEYWORD_PATTERNS = [
"always\\s+remember",
];

const DEFAULT_COMPACTION_THRESHOLD = 0.8;

const DEFAULTS: Required<Omit<SupermemoryConfig, "apiKey" | "baseUrl" | "apiVersion" | "userContainerTag" | "projectContainerTag" | "recallDirective">> = {
similarityThreshold: 0.55,
maxMemories: 5,
Expand All @@ -66,7 +72,7 @@ const DEFAULTS: Required<Omit<SupermemoryConfig, "apiKey" | "baseUrl" | "apiVers
containerTagPrefix: "opencode",
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.",
keywordPatterns: [],
compactionThreshold: 0.80,
compactionThreshold: DEFAULT_COMPACTION_THRESHOLD,
compactionEnabled: true,
autoRecallEveryPrompt: false,
captureEveryNTurns: 0,
Expand All @@ -82,14 +88,26 @@ function isValidRegex(pattern: string): boolean {
}
}

function validateCompactionThreshold(value: number | undefined): number {
if (value === undefined || typeof value !== 'number' || isNaN(value)) {
return DEFAULTS.compactionThreshold;
export function validateCompactionThreshold(
value: number | false | undefined,
): number {
if (value === false || value === 0) return 0;
if (value === undefined || typeof value !== "number" || Number.isNaN(value)) {
return DEFAULT_COMPACTION_THRESHOLD;
}
if (value <= 0 || value > 1) return DEFAULTS.compactionThreshold;
if (value < 0 || value > 1) return DEFAULT_COMPACTION_THRESHOLD;
return value;
}

/** `compactionEnabled` wins; otherwise a legacy threshold of 0 or false turns it off. */
export function resolveCompactionEnabled(
enabled: unknown,
legacyThreshold: number | false | undefined,
): boolean {
if (typeof enabled === "boolean") return enabled;
return validateCompactionThreshold(legacyThreshold) !== 0;
}

function validateCaptureEveryNTurns(
value: number | undefined,
fallback: number,
Expand Down Expand Up @@ -200,10 +218,10 @@ export const CONFIG = {
...(fileConfig.keywordPatterns ?? []).filter(isValidRegex),
],
compactionThreshold: validateCompactionThreshold(fileConfig.compactionThreshold),
compactionEnabled:
typeof fileConfig.compactionEnabled === "boolean"
? fileConfig.compactionEnabled
: DEFAULTS.compactionEnabled,
compactionEnabled: resolveCompactionEnabled(
fileConfig.compactionEnabled,
fileConfig.compactionThreshold,
),
autoRecallEveryPrompt:
fileConfig.autoRecallEveryPrompt ??
(configExisted ? true : DEFAULTS.autoRecallEveryPrompt),
Expand Down
38 changes: 8 additions & 30 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,44 +89,22 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
log("Plugin disabled - SUPERMEMORY_API_KEY not set");
}

// Fetch model limits once at plugin init
const modelLimits = new Map<string, number>();

(async () => {
try {
const response = await ctx.client.provider.list();
if (response.data?.all) {
for (const provider of response.data.all) {
if (provider.models) {
for (const [modelId, model] of Object.entries(provider.models)) {
if (model.limit?.context) {
modelLimits.set(`${provider.id}/${modelId}`, model.limit.context);
}
}
}
}
}
log("Model limits loaded", { count: modelLimits.size });
} catch (error) {
log("Failed to fetch model limits", { error: String(error) });
}
})();

const getModelLimit = (providerID: string, modelID: string): number | undefined => {
return modelLimits.get(`${providerID}/${modelID}`);
};

const compactionHook = isConfigured() && ctx.client
const compactionHook = isConfigured() && ctx.client && CONFIG.compactionEnabled
? createCompactionHook(ctx as CompactionContext, tags, {
threshold: CONFIG.compactionThreshold,
getModelLimit,
onSaved: () => activity.saved(),
})
: null;
const captureHook = isConfigured() && ctx.client
? createCaptureHook(ctx, tags, { onSaved: () => activity.saved() })
: null;

return {
"experimental.session.compacting": compactionHook
? async (input, output) => {
await compactionHook.compacting(input, output);
}
: undefined,

"chat.message": async (input, output) => {
if (!isConfigured()) return;

Expand Down
Loading
Loading