Skip to content
Closed
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
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Install dependencies
run: bun install

- name: Type check
run: bun run typecheck

- name: Test
run: bun test

- name: Build
run: bun run build
91 changes: 61 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ bunx opencode-supermemory@latest status # check the connection any time
Install opencode-supermemory by following https://raw.githubusercontent.com/supermemoryai/opencode-supermemory/main/README.md
```

`install`, `login`, `logout`, and `status` are also available as in-chat commands
(`/supermemory-init`, `/supermemory-login`, `/supermemory-logout`, `/supermemory-status`)
once the plugin is registered.
`login`, `logout`, and `status` are also available as in-chat commands
(`/supermemory-login`, `/supermemory-logout`, `/supermemory-status`) once the plugin is
registered, and `/supermemory-index` (alias `/supermemory-init`) indexes the codebase.

<details>
<summary>Instructions for LLM agents</summary>
Expand All @@ -55,15 +55,19 @@ If you're an LLM agent helping set up opencode-supermemory, follow these steps:
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.
The installer creates `~/.config/opencode` if needed and then:

- Register the plugin in `~/.config/opencode/opencode.jsonc` (`plugin` for V1, `plugins`
- Registers 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)
- Create the `/supermemory-index` command (`/supermemory-init` is an alias)
- Enables the V1 status footer in `~/.config/opencode/tui.jsonc`
- Writes defaults into the existing `supermemory.jsonc`/`supermemory.json` (or creates
`supermemory.json`), preserving comments and any keys already there
- Creates the `/supermemory-index`, `/supermemory-init`, `/supermemory-login`,
`/supermemory-logout`, and `/supermemory-status` commands

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.

#### Step 2: Verify the config

Expand Down Expand Up @@ -125,24 +129,34 @@ 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. |
| 🧠 **Profile context**<br>On a session's first message, the agent silently receives your Supermemory profile: stable facts about you plus recent context. Everything else is recalled per prompt. | 🔎 **Direct recall**<br>On every substantive prompt the plugin searches your personal and project memories and injects up to five fresh matches. Set `recallMode: "advisory"` to let the model decide when to search instead; that search 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. |
| 🔒 **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. |

First message of a session:

```
[SUPERMEMORY]
Every line marked ◪ comes from supermemory. When one shapes your answer, credit it naturally with the ◪ prefix; if you name the source, say "from supermemory".

User Profile:
- Prefers concise responses
- Expert in TypeScript
- ◪ Prefers concise responses
- ◪ Expert in TypeScript

Project Knowledge:
- [100%] Uses Bun, not Node.js
- [100%] Build: bun run build
Recent Context:
- ◪ Migrating the auth service to Bun
```

Relevant Memories:
- [82%] Build fails if .env.local missing
Any substantive prompt (direct recall):

```
<supermemory-context>
Relevant memories automatically recalled for this prompt. Every line marked ◪ comes from supermemory:
- ◪ Uses Bun, not Node.js
- ◪ Build fails if .env.local is missing
Use these memories only when relevant. Search Supermemory for deeper context if needed.
</supermemory-context>
```

The agent uses this context automatically - no manual prompting needed.
Expand Down Expand Up @@ -259,13 +273,14 @@ The `supermemory` tool is available to the agent:
| Mode | Args | Description |
| --- | --- | --- |
| `add` | `content`, `type?`, `scope?` | Store memory |
| `search` | `query`, `scope?` | Search memories |
| `search` | `query`, `scope?`, `limit?` | Search memories |
| `profile` | `query?` | View user profile |
| `list` | `scope?`, `limit?` | List memories |
| `forget` | `memoryId`, `scope?` | Delete memory |
| `help` | none | List available modes |

**Scopes:** `user` (personal memories for the current project), `project` (default)
**Scopes:** `user` (personal memories for the current project) and `project`. `search`
without a scope covers both; `add`, `list`, and `forget` default to `project`.

**Types:** `project-config`, `architecture`, `error-solution`, `preference`, `learned-pattern`, `conversation`

Expand Down Expand Up @@ -300,9 +315,9 @@ does not require a migration.
| `SUPERMEMORY_API_URL` / `SUPERMEMORY_BASE_URL` | Override the Supermemory API base URL. |
| `SUPERMEMORY_AUTH_URL` | Override the browser-auth base URL. |
| `SUPERMEMORY_AUTH_TIMEOUT` | Browser-auth timeout in milliseconds (default 5 minutes). |
| `SUPERMEMORY_REPO_TAG` | Explicit project-container override, checked before the config value. |
| `SUPERMEMORY_REPO_TAG` | Explicit project-container override (see [precedence](#container-tag-selection)). |
| `SUPERMEMORY_ISOLATE_WORKTREES` | Set to `true` to key the project container on the worktree path instead of the Git remote. |
| `SUPERMEMORY_DEBUG` | Set to show `[recall-decision]` lines and enable debug logging. |
| `SUPERMEMORY_DEBUG` | In advisory mode, asks the model to print a `[recall-decision]` line each reply. File logging to `~/.opencode-supermemory.log` is always on. |

### `~/.config/opencode/supermemory.jsonc`

Expand All @@ -317,10 +332,11 @@ does not require a migration.
// Min similarity for memory retrieval (0-1)
"similarityThreshold": 0.55,

// Max memories injected per request
// Results fetched per memory search (tool searches and direct recall reads).
// Direct recall injects at most 5 after similarity filtering and dedupe.
"maxMemories": 5,

// Max project memories listed
// Project memories added to compaction summaries
"maxProjectMemories": 10,

// Max profile facts injected
Expand All @@ -329,11 +345,14 @@ does not require a migration.
// Include user profile in context
"injectProfile": true,

// Also run a semantic search over personal memories on a session's first
// message, not just profile + project list (default: true on upgrades,
// false on fresh installs)
// Legacy switch, only read when recallMode is unset:
// true maps to "direct", false maps to "advisory"
"autoRecallEveryPrompt": true,

// Instructions for Supermemory's server-side LLM filter. When the plugin
// connects it enables the filter on your account with this prompt.
"filterPrompt": "You are a stateful coding agent. Remember the user's coding preferences, tech stack, behaviours, and workflows.",

// Legacy prefix retained when reading containers made by older versions
"containerTagPrefix": "opencode",

Expand All @@ -352,7 +371,8 @@ does not require a migration.
// OpenCode 2: enrich native compaction with project memories and save summaries
"compactionEnabled": true,

// Save completed conversation batches every N turns (0 = session end only)
// Save completed conversation batches every N turns (0 = session end only).
// Default: 0 on fresh installs, 3 when a config file exists without this key.
"captureEveryNTurns": 3,

// "direct" (default for new installs), "advisory", or "off"
Expand All @@ -373,8 +393,19 @@ By default, new writes use:
- No origin remote: `repo_{project-name}__{hash(real-repository-path)}`

Older `{prefix}_user_*` and `{prefix}_project_*` containers remain readable.
`userContainerTag` is treated as a legacy personal read. You can still override the
unified write container with `projectContainerTag`:
`userContainerTag` is treated as a legacy personal read.

The write container is resolved from the first of these that is set, so a Claude Code or
Cursor project config for the same repository wins over this plugin's own setting:

1. `repoContainerTag` in the repository's `.claude/.supermemory-claude/config.json`
2. `SUPERMEMORY_REPO_TAG`
3. `repoContainerTag` in the repository's legacy Cursor plugin config under `.cursor/`
4. `projectContainerTag` in `~/.config/opencode/supermemory.jsonc`
5. `projectContainerTag` in `~/.codex/supermemory.json`
6. The generated `repo_{project-name}__{hash}` tag

You can still override the unified write container with `projectContainerTag`:

```jsonc
{
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opencode-supermemory",
"version": "2.0.15",
"version": "2.0.16",
"description": "OpenCode plugin that gives coding agents persistent memory using Supermemory",
"type": "module",
"main": "dist/index.js",
Expand Down
29 changes: 13 additions & 16 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { join } from "node:path";
import { homedir } from "node:os";
import * as readline from "node:readline";
import { startAuthFlow, clearCredentials, loadCredentials, CREDENTIALS_FILE } from "./services/auth.js";
import { CONFIG, CONFIG_FILE, SUPERMEMORY_API_KEY, getApiBaseUrl, isConfigured, writeInstallDefaults } from "./config.js";
import { CONFIG, CONFIG_DIR, SUPERMEMORY_API_KEY, getApiBaseUrl, getConfigFilePath, isConfigured, readConfigFile } from "./config.js";
import { writeInstallDefaults } from "./services/install-defaults.js";
import { SupermemoryClient } from "./services/client.js";
import { getTags } from "./services/tags.js";
import {
Expand All @@ -16,7 +17,7 @@ import {
V2_PLUGIN_ENTRY,
} from "./services/opencode-config.js";

const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode");
const OPENCODE_CONFIG_DIR = CONFIG_DIR;
const OPENCODE_COMMAND_DIRS = [
join(OPENCODE_CONFIG_DIR, "commands"),
join(OPENCODE_CONFIG_DIR, "command"),
Expand All @@ -26,7 +27,6 @@ const OPENCODE_TUI_CONFIGS = [
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 = `---
description: Index this codebase into Supermemory
Expand Down Expand Up @@ -209,7 +209,7 @@ bunx opencode-supermemory@latest login
\`\`\`

This will:
1. Start a local server on port 19877
1. Start a local callback server on a free port
2. Open the browser to Supermemory's authentication page
3. After the user logs in, save credentials to ~/.supermemory-opencode/credentials.json

Expand Down Expand Up @@ -416,7 +416,12 @@ interface InstallOptions {
async function install(options: InstallOptions): Promise<number> {
console.log("\n🧠 opencode-supermemory installer\n");

writeInstallDefaults(existsSync(DEFAULT_CONFIG_FILE));
try {
const defaults = writeInstallDefaults(OPENCODE_CONFIG_DIR);
if (defaults.changed) console.log(`✓ Wrote Supermemory defaults to ${defaults.path}`);
} catch (err) {
console.error("✗ Failed to update the Supermemory config:", err);
}

const rl = options.tui ? createReadline() : null;

Expand Down Expand Up @@ -532,19 +537,10 @@ function maskKey(key: string | undefined): string {
return `${key.slice(0, 6)}...${key.slice(-4)}`;
}

function getConfiguredApiKeyFromFile(): string | undefined {
try {
if (!existsSync(DEFAULT_CONFIG_FILE)) return undefined;
const parsed = JSON.parse(readFileSync(DEFAULT_CONFIG_FILE, "utf-8")) as { apiKey?: string };
return parsed.apiKey;
} catch {
return undefined;
}
}

function getKeySource(): string {
if (process.env.SUPERMEMORY_API_KEY) return "SUPERMEMORY_API_KEY env var";
if (getConfiguredApiKeyFromFile()) return DEFAULT_CONFIG_FILE;
const configPath = getConfigFilePath();
if (configPath && readConfigFile(configPath)?.apiKey) return configPath;
if (loadCredentials()) return CREDENTIALS_FILE;
return "not configured";
}
Expand Down Expand Up @@ -631,6 +627,7 @@ async function status(): Promise<number> {
lines.push(`Connected: ${isConfigured() ? "checking..." : "no"}`);
lines.push(`API key: ${maskKey(SUPERMEMORY_API_KEY)} (${getKeySource()})`);
lines.push(`API URL: ${apiUrl}`);
lines.push(`Config file: ${getConfigFilePath() ?? `none (defaults; create ${join(OPENCODE_CONFIG_DIR, "supermemory.jsonc")})`}`);
lines.push("Memory scope: unified project container with personal/project metadata");
lines.push(`Recall mode: ${CONFIG.recallMode}`);
lines.push(`Recall directive: ${CONFIG.recallMode === "advisory" && CONFIG.recallDirective ? "custom" : "default"}`);
Expand Down
48 changes: 19 additions & 29 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { stripJsoncComments } from "./services/jsonc.js";
import { loadCredentials } from "./services/auth.js";

const CONFIG_DIR = join(homedir(), ".config", "opencode");
export const CONFIG_DIR = join(homedir(), ".config", "opencode");
export { PLUGIN_VERSION } from "./version.js";
const CONFIG_FILES = [
join(CONFIG_DIR, "supermemory.jsonc"),
Expand Down Expand Up @@ -118,19 +118,24 @@ function resolveRecallMode(): RecallMode {
return DEFAULTS.recallMode;
}

function loadRawConfig(): { config: SupermemoryConfig; existed: boolean } {
for (const path of CONFIG_FILES) {
if (existsSync(path)) {
try {
const content = readFileSync(path, "utf-8");
const json = stripJsoncComments(content);
return { config: JSON.parse(json) as SupermemoryConfig, existed: true };
} catch {
return { config: {}, existed: true };
}
}
/** The config file the plugin reads: `supermemory.jsonc` wins over `supermemory.json`. */
export function getConfigFilePath(): string | undefined {
return CONFIG_FILES.find((path) => existsSync(path));
}

/** Parses a Supermemory config file (JSON or JSONC). Returns undefined when unreadable. */
export function readConfigFile(path: string): SupermemoryConfig | undefined {
try {
return JSON.parse(stripJsoncComments(readFileSync(path, "utf-8"))) as SupermemoryConfig;
} catch {
return undefined;
}
return { config: {}, existed: false };
}

function loadRawConfig(): { config: SupermemoryConfig; existed: boolean } {
const path = getConfigFilePath();
if (!path) return { config: {}, existed: false };
return { config: readConfigFile(path) ?? {}, existed: true };
}

const { config: fileConfig, existed: configExisted } = loadRawConfig();
Expand Down Expand Up @@ -172,9 +177,6 @@ export function getApiBaseUrl(): string {
return normalized;
}

export const CONFIG_FILE = CONFIG_FILES[1];
const DEFAULT_CONFIG_FILE = CONFIG_FILE ?? join(CONFIG_DIR, "supermemory.json");

export const CONFIG = {
similarityThreshold: fileConfig.similarityThreshold ?? DEFAULTS.similarityThreshold,
maxMemories: fileConfig.maxMemories ?? DEFAULTS.maxMemories,
Expand Down Expand Up @@ -218,15 +220,3 @@ export function getRecallConfig(): {
mode: CONFIG.recallMode,
};
}

export function writeInstallDefaults(isExistingInstall: boolean): void {
const current = loadRawConfig().config;
const next: SupermemoryConfig = { ...current };
if (isExistingInstall) {
if (next.captureEveryNTurns === undefined) next.captureEveryNTurns = 3;
} else {
next.recallMode = "direct";
next.captureEveryNTurns = 0;
}
writeFileSync(DEFAULT_CONFIG_FILE, JSON.stringify(next, null, 2));
}
3 changes: 2 additions & 1 deletion src/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { arch, homedir, hostname, platform } from "node:os";
import { randomBytes } from "node:crypto";
import type { AddressInfo } from "node:net";
import { openUrl } from "./openUrl.js";
import { PLUGIN_VERSION } from "../version.js";

const CREDENTIALS_DIR = join(homedir(), ".supermemory-opencode");
export const CREDENTIALS_FILE = join(CREDENTIALS_DIR, "credentials.json");
Expand Down Expand Up @@ -162,7 +163,7 @@ export function startAuthFlow(timeoutMs = AUTH_TIMEOUT): Promise<AuthResult> {
hostname: `opencode - ${hostname()}`,
os: `${platform()}-${arch()}`,
cwd: process.cwd(),
cli_version: "2.0.10",
cli_version: PLUGIN_VERSION,
});
const authUrl = `${AUTH_BASE_URL}?${params.toString()}`;

Expand Down
Loading
Loading