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
2 changes: 1 addition & 1 deletion bun.lock

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"url": "https://github.com/supermemoryai/opencode-supermemory"
},
"devDependencies": {
"@opencode-ai/plugin": "^1.0.162",
"@opencode-ai/plugin": "^1.0.191",
"@types/bun": "latest",
"supermemory": "^4.0.0",
"typescript": "^5.7.3"
Expand All @@ -40,6 +40,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 @@ -11,7 +11,6 @@ import { getTags } from "./services/tags.js";

const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode");
const OPENCODE_COMMAND_DIR = join(OPENCODE_CONFIG_DIR, "command");
const OH_MY_OPENCODE_CONFIG = join(OPENCODE_CONFIG_DIR, "oh-my-opencode.json");
const PLUGIN_NAME = "opencode-supermemory@latest";
const DEFAULT_CONFIG_FILE = CONFIG_FILE ?? join(OPENCODE_CONFIG_DIR, "supermemory.json");

Expand Down Expand Up @@ -344,58 +343,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 @@ -446,33 +395,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 @@ -654,7 +579,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 @@ -676,13 +600,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
31 changes: 25 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const CONFIG_FILES = [
];

export const DEFAULT_BASE_URL = "https://api.supermemory.ai";
const DEFAULT_COMPACTION_THRESHOLD = 0.8;

interface SupermemoryConfig {
apiKey?: string;
Expand All @@ -26,7 +27,9 @@ interface SupermemoryConfig {
projectContainerTag?: string;
filterPrompt?: string;
keywordPatterns?: string[];
compactionThreshold?: number;
compactionEnabled?: boolean;
/** @deprecated OpenCode now owns the compaction trigger. Use compactionEnabled. */
compactionThreshold?: number | false;
autoRecallEveryPrompt?: boolean;
captureEveryNTurns?: number;
recallDirective?: string | null;
Expand Down Expand Up @@ -60,7 +63,8 @@ const DEFAULTS: Required<Omit<SupermemoryConfig, "apiKey" | "baseUrl" | "userCon
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,
compactionEnabled: true,
compactionThreshold: DEFAULT_COMPACTION_THRESHOLD,
autoRecallEveryPrompt: false,
captureEveryNTurns: 0,
};
Expand All @@ -74,14 +78,25 @@ 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;
}

export function resolveCompactionEnabled(
enabled: boolean | undefined,
legacyThreshold: number | false | undefined,
): boolean {
if (enabled !== undefined) return enabled;
return validateCompactionThreshold(legacyThreshold) !== 0;
}

function validateCaptureEveryNTurns(
value: number | undefined,
fallback: number,
Expand Down Expand Up @@ -168,6 +183,10 @@ export const CONFIG = {
...DEFAULT_KEYWORD_PATTERNS,
...(fileConfig.keywordPatterns ?? []).filter(isValidRegex),
],
compactionEnabled: resolveCompactionEnabled(
fileConfig.compactionEnabled,
fileConfig.compactionThreshold,
),
compactionThreshold: validateCompactionThreshold(fileConfig.compactionThreshold),
autoRecallEveryPrompt:
fileConfig.autoRecallEveryPrompt ??
Expand Down
40 changes: 8 additions & 32 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,44 +73,20 @@ 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
? createCompactionHook(ctx as CompactionContext, tags, {
threshold: CONFIG.compactionThreshold,
getModelLimit,
})
const compactionHook = isConfigured() && ctx.client && CONFIG.compactionEnabled
? createCompactionHook(ctx as CompactionContext, tags)
: null;
const captureHook = isConfigured() && ctx.client
? createCaptureHook(ctx, tags)
: 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