From eb2f5c836617849eb185a4d07b1048bbea004c6f Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Mon, 28 Sep 2026 00:46:34 +0530 Subject: [PATCH] Fix CLI config handling, login version, and README drift - The CLI now uses the same config file the plugin reads (`supermemory.jsonc` first, then `supermemory.json`). `status` reports that path as the key source, and `install` edits the existing file in place with comments preserved instead of writing a second `supermemory.json` that duplicated the API key and never took effect. Fixes #91. - `install` creates `~/.config/opencode` before writing defaults, so a first run on a fresh machine no longer crashes with ENOENT. Fixes #93. - Login reports the real `PLUGIN_VERSION` as `cli_version` instead of a hardcoded 2.0.10. Fixes #95. - README describes what the plugin does today: profile-only first-message context, direct recall by default, the real injected blocks, tool scopes and `limit`, container tag precedence, `filterPrompt`, capture defaults, and `SUPERMEMORY_DEBUG`. The generated login command no longer claims a fixed port, and the tool's help lists `limit` for search. Fixes #97. - Run typecheck, tests, and build on pull requests and pushes to main. --- .github/workflows/ci.yml | 29 +++++++++ README.md | 91 ++++++++++++++++++--------- package.json | 2 +- src/cli.ts | 29 ++++----- src/config.ts | 48 ++++++-------- src/services/auth.ts | 3 +- src/services/install-defaults.test.ts | 61 ++++++++++++++++++ src/services/install-defaults.ts | 87 +++++++++++++++++++++++++ src/services/memory-tool.ts | 4 +- src/version.ts | 2 +- 10 files changed, 276 insertions(+), 80 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 src/services/install-defaults.test.ts create mode 100644 src/services/install-defaults.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..af61417 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index cd331ed..dca35ee 100644 --- a/README.md +++ b/README.md @@ -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.
Instructions for LLM agents @@ -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 @@ -125,24 +129,34 @@ Run `/supermemory-index` to have the agent explore and memorize the codebase. `/ | | | | --- | --- | -| 🧠 **Context injection**
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**
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**
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**
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**
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 `` content is redacted. | šŸ—£ļø **Keyword detection**
Saying "remember", "save this", "don't forget", or a custom pattern nudges the agent to save to memory. | | 🧭 **Codebase indexing**
`/supermemory-init` has the agent explore and memorize the codebase's structure, patterns, and conventions. | šŸ—œļø **Compaction memory**
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**
Content wrapped in `...` is never stored. | šŸ”” **Update notices**
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): + +``` + +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. + ``` The agent uses this context automatically - no manual prompting needed. @@ -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` @@ -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` @@ -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 @@ -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", @@ -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" @@ -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 { diff --git a/package.json b/package.json index 270fe74..15bc0cb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/cli.ts b/src/cli.ts index 686c7d5..92786b8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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 { @@ -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"), @@ -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 @@ -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 @@ -416,7 +416,12 @@ interface InstallOptions { async function install(options: InstallOptions): Promise { 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; @@ -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"; } @@ -631,6 +627,7 @@ async function status(): Promise { 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"}`); diff --git a/src/config.ts b/src/config.ts index b342f7b..cf68475 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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"), @@ -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(); @@ -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, @@ -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)); -} diff --git a/src/services/auth.ts b/src/services/auth.ts index bacfac8..c6ef20e 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -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"); @@ -162,7 +163,7 @@ export function startAuthFlow(timeoutMs = AUTH_TIMEOUT): Promise { 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()}`; diff --git a/src/services/install-defaults.test.ts b/src/services/install-defaults.test.ts new file mode 100644 index 0000000..e952a4b --- /dev/null +++ b/src/services/install-defaults.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test } from "bun:test"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { + applyInstallDefaults, + resolveConfigFilePath, + writeInstallDefaults, +} from "./install-defaults.js"; + +describe("installer config defaults", () => { + test("prefers supermemory.jsonc and falls back to supermemory.json", () => { + const dir = mkdtempSync(join(tmpdir(), "sm-config-")); + try { + expect(resolveConfigFilePath(dir)).toBe(join(dir, "supermemory.json")); + writeFileSync(join(dir, "supermemory.jsonc"), "{}\n"); + expect(resolveConfigFilePath(dir)).toBe(join(dir, "supermemory.jsonc")); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + test("edits an existing JSONC file in place without copying or dropping anything", () => { + const original = '{\n // per README\n "apiKey": "sm_test_key_1234"\n}\n'; + const result = applyInstallDefaults(original, true); + expect(result.changed).toBe(true); + expect(result.content).toContain("// per README"); + expect(result.content).toContain('"apiKey": "sm_test_key_1234"'); + expect(result.content).toContain('"captureEveryNTurns": 3'); + expect(result.content).not.toContain("recallMode"); + expect(applyInstallDefaults(result.content, true).changed).toBe(false); + }); + + test("fresh installs get direct recall and session-end capture, existing values win", () => { + const fresh = JSON.parse(applyInstallDefaults("", false).content); + expect(fresh).toEqual({ recallMode: "direct", captureEveryNTurns: 0 }); + + const custom = applyInstallDefaults('{"recallMode": "advisory"}', false); + expect(JSON.parse(custom.content)).toEqual({ recallMode: "advisory", captureEveryNTurns: 0 }); + }); + + test("creates the config directory and writes only the file the plugin reads", () => { + const home = mkdtempSync(join(tmpdir(), "sm-home-")); + const dir = join(home, ".config", "opencode"); + try { + const first = writeInstallDefaults(dir); + expect(first.path).toBe(join(dir, "supermemory.json")); + expect(existsSync(first.path)).toBe(true); + + rmSync(first.path); + writeFileSync(join(dir, "supermemory.jsonc"), '{\n // keep me\n "apiKey": "sm_x"\n}\n'); + const second = writeInstallDefaults(dir); + expect(second.path).toBe(join(dir, "supermemory.jsonc")); + expect(existsSync(join(dir, "supermemory.json"))).toBe(false); + expect(readFileSync(second.path, "utf-8")).toContain("// keep me"); + } finally { + rmSync(home, { recursive: true, force: true }); + } + }); +}); diff --git a/src/services/install-defaults.ts b/src/services/install-defaults.ts new file mode 100644 index 0000000..2a88bcc --- /dev/null +++ b/src/services/install-defaults.ts @@ -0,0 +1,87 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +import { applyEdits, modify, parse, type ParseError } from "jsonc-parser/lib/esm/main.js"; + +/** Candidate config files, in the order the plugin reads them. */ +export const CONFIG_FILE_NAMES = ["supermemory.jsonc", "supermemory.json"] as const; + +/** + * The config file the plugin will actually read: the first existing candidate, + * or `supermemory.json` when none exists yet. + */ +export function resolveConfigFilePath(configDir: string): string { + for (const name of CONFIG_FILE_NAMES) { + const path = join(configDir, name); + if (existsSync(path)) return path; + } + return join(configDir, CONFIG_FILE_NAMES[1]); +} + +export interface InstallDefaultsResult { + content: string; + changed: boolean; +} + +function parseJsonc(content: string): Record { + const errors: ParseError[] = []; + const value = parse(content, errors, { allowTrailingComma: true }); + if (errors.length > 0) { + throw new Error(`Invalid Supermemory config at offset ${errors[0]!.offset}`); + } + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error("Supermemory config must contain a JSON object"); + } + return value as Record; +} + +function formattingOptions(content: string) { + const eol = content.includes("\r\n") ? "\r\n" : "\n"; + const indent = content.match(/\r?\n([ \t]+)"/)?.[1]; + const usesTabs = indent?.includes("\t") ?? false; + return { eol, insertSpaces: !usesTabs, tabSize: usesTabs ? 1 : Math.max(2, indent?.length ?? 2) }; +} + +/** + * Applies the installer defaults to existing config content without touching + * anything else, so comments and the user's other keys survive. A fresh + * install gets `recallMode: "direct"` and session-end-only capture; an + * existing install keeps its recall mode and only fills in the capture + * cadence older versions assumed. + */ +export function applyInstallDefaults( + rawContent: string, + isExistingInstall: boolean, +): InstallDefaultsResult { + let content = rawContent.trim() === "" ? "{}\n" : rawContent; + const current = parseJsonc(content); + const updates: Array<[string, unknown]> = []; + + if (isExistingInstall) { + if (current.captureEveryNTurns === undefined) updates.push(["captureEveryNTurns", 3]); + } else { + if (current.recallMode === undefined) updates.push(["recallMode", "direct"]); + if (current.captureEveryNTurns === undefined) updates.push(["captureEveryNTurns", 0]); + } + + for (const [key, value] of updates) { + content = applyEdits( + content, + modify(content, [key], value, { formattingOptions: formattingOptions(content) }), + ); + } + return { content, changed: content !== rawContent }; +} + +/** + * Writes installer defaults into the config file the plugin reads, creating + * the config directory when needed. Returns the path that was used. + */ +export function writeInstallDefaults(configDir: string): { path: string; changed: boolean } { + mkdirSync(configDir, { recursive: true }); + const path = resolveConfigFilePath(configDir); + const exists = existsSync(path); + const result = applyInstallDefaults(exists ? readFileSync(path, "utf-8") : "", exists); + if (result.changed) writeFileSync(path, result.content); + return { path, changed: result.changed }; +} diff --git a/src/services/memory-tool.ts b/src/services/memory-tool.ts index 53793f9..145cd2e 100644 --- a/src/services/memory-tool.ts +++ b/src/services/memory-tool.ts @@ -95,8 +95,8 @@ export async function executeSupermemoryTool( }, { command: "search", - description: "Search memories", - args: ["query", "scope?"], + description: "Search memories (both scopes unless scope is given)", + args: ["query", "scope?", "limit?"], }, { command: "profile", diff --git a/src/version.ts b/src/version.ts index df3ccbb..33a1c36 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,3 +1,3 @@ // AUTO-GENERATED by scripts/sync-version.mjs — do not edit by hand. // Sourced from package.json "version" so the two cannot drift. -export const PLUGIN_VERSION = "2.0.15"; +export const PLUGIN_VERSION = "2.0.16";