From 51de4b04be461d49888dc994ad9ed62e15bf5adc Mon Sep 17 00:00:00 2001 From: Omkar Chebale Date: Sun, 27 Sep 2026 19:06:55 +0530 Subject: [PATCH 1/2] Use the loaded supermemory config file in install and status The plugin loads supermemory.jsonc before supermemory.json, but the CLI always used supermemory.json. With only a .jsonc (as the README suggests), install treated the setup as fresh and wrote a stray supermemory.json with a copy of the API key and different settings that never took effect, and status reported the key source as not configured. Point CONFIG_FILE at the file that was actually loaded, apply install defaults with jsonc-parser so comments are preserved (and leave unparseable files alone), and parse the file as JSONC in status. --- src/cli.ts | 3 ++- src/config.test.ts | 36 +++++++++++++++++++++++++++++++++ src/config.ts | 50 +++++++++++++++++++++++++++++++++------------- 3 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 src/config.test.ts diff --git a/src/cli.ts b/src/cli.ts index 686c7d5..97c32d2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,6 +7,7 @@ import { startAuthFlow, clearCredentials, loadCredentials, CREDENTIALS_FILE } fr import { CONFIG, CONFIG_FILE, SUPERMEMORY_API_KEY, getApiBaseUrl, isConfigured, writeInstallDefaults } from "./config.js"; import { SupermemoryClient } from "./services/client.js"; import { getTags } from "./services/tags.js"; +import { stripJsoncComments } from "./services/jsonc.js"; import { editOpenCodeConfig, editOpenCodeTuiConfig, @@ -535,7 +536,7 @@ function maskKey(key: string | undefined): string { 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 }; + const parsed = JSON.parse(stripJsoncComments(readFileSync(DEFAULT_CONFIG_FILE, "utf-8"))) as { apiKey?: string }; return parsed.apiKey; } catch { return undefined; diff --git a/src/config.test.ts b/src/config.test.ts new file mode 100644 index 0000000..b03a89c --- /dev/null +++ b/src/config.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from "bun:test"; + +import { applyInstallDefaults } from "./config.js"; + +const JSONC = `{ + // created per README + "apiKey": "sm_test", + "recallMode": "advisory" +} +`; + +describe("applyInstallDefaults", () => { + test("keeps comments and user settings in an existing jsonc config", () => { + const next = applyInstallDefaults(JSONC, true)!; + + expect(next).toContain("// created per README"); + expect(next).toContain('"recallMode": "advisory"'); + expect(next).toContain('"captureEveryNTurns": 3'); + }); + + test("leaves an existing capture cadence untouched", () => { + const content = `{ "captureEveryNTurns": 5 } // keep`; + expect(applyInstallDefaults(content, true)).toBe(content); + }); + + test("writes fresh-install defaults to an empty config", () => { + expect(JSON.parse(applyInstallDefaults("", false)!)).toEqual({ + recallMode: "direct", + captureEveryNTurns: 0, + }); + }); + + test("does not rewrite a config it cannot parse", () => { + expect(applyInstallDefaults(`{ "apiKey": `, true)).toBeNull(); + }); +}); diff --git a/src/config.ts b/src/config.ts index b342f7b..252f18b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,6 +3,7 @@ import { join } from "node:path"; import { homedir } from "node:os"; import { stripJsoncComments } from "./services/jsonc.js"; import { loadCredentials } from "./services/auth.js"; +import { applyEdits, modify, parse, type ParseError } from "jsonc-parser/lib/esm/main.js"; const CONFIG_DIR = join(homedir(), ".config", "opencode"); export { PLUGIN_VERSION } from "./version.js"; @@ -118,22 +119,22 @@ function resolveRecallMode(): RecallMode { return DEFAULTS.recallMode; } -function loadRawConfig(): { config: SupermemoryConfig; existed: boolean } { +function loadRawConfig(): { config: SupermemoryConfig; existed: boolean; path?: string } { 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 }; + return { config: JSON.parse(json) as SupermemoryConfig, existed: true, path }; } catch { - return { config: {}, existed: true }; + return { config: {}, existed: true, path }; } } } return { config: {}, existed: false }; } -const { config: fileConfig, existed: configExisted } = loadRawConfig(); +const { config: fileConfig, existed: configExisted, path: loadedConfigFile } = loadRawConfig(); function getApiKey(): string | undefined { if (process.env.SUPERMEMORY_API_KEY) return process.env.SUPERMEMORY_API_KEY; @@ -172,7 +173,8 @@ export function getApiBaseUrl(): string { return normalized; } -export const CONFIG_FILE = CONFIG_FILES[1]; +/** The file the config was loaded from, or where a new one should be created. */ +export const CONFIG_FILE = loadedConfigFile ?? CONFIG_FILES[1]; const DEFAULT_CONFIG_FILE = CONFIG_FILE ?? join(CONFIG_DIR, "supermemory.json"); export const CONFIG = { @@ -219,14 +221,34 @@ export function getRecallConfig(): { }; } -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; +/** + * Adds install defaults to raw config content without rewriting the rest of + * the file, so comments in supermemory.jsonc survive. Returns null when the + * content is not valid JSONC and should be left alone. + */ +export function applyInstallDefaults(rawContent: string, isExistingInstall: boolean): string | null { + const content = rawContent.trim() === "" ? "{}\n" : rawContent; + const errors: ParseError[] = []; + const current = parse(content, errors, { allowTrailingComma: true }) as SupermemoryConfig | undefined; + if (errors.length > 0 || typeof current !== "object" || current === null || Array.isArray(current)) { + return null; + } + + const defaults: SupermemoryConfig = isExistingInstall + ? current.captureEveryNTurns === undefined ? { captureEveryNTurns: 3 } : {} + : { recallMode: "direct", captureEveryNTurns: 0 }; + + let next = content; + for (const [key, value] of Object.entries(defaults)) { + next = applyEdits(next, modify(next, [key], value, { + formattingOptions: { insertSpaces: true, tabSize: 2 }, + })); } - writeFileSync(DEFAULT_CONFIG_FILE, JSON.stringify(next, null, 2)); + return next; +} + +export function writeInstallDefaults(isExistingInstall: boolean): void { + const raw = existsSync(DEFAULT_CONFIG_FILE) ? readFileSync(DEFAULT_CONFIG_FILE, "utf-8") : ""; + const next = applyInstallDefaults(raw, isExistingInstall); + if (next !== null && next !== raw) writeFileSync(DEFAULT_CONFIG_FILE, next); } From 68fe6a6cc8840d76c88fb44cc7155d42fd61ef49 Mon Sep 17 00:00:00 2001 From: Omkar Chebale Date: Sun, 27 Sep 2026 19:09:58 +0530 Subject: [PATCH 2/2] Move install-defaults editing to the CLI Keeps jsonc-parser out of the plugin runtime bundles, since only install needs it, and warns instead of silently skipping when the existing config cannot be parsed. --- src/cli.ts | 13 +++++++++- src/config.test.ts | 36 ---------------------------- src/config.ts | 36 +--------------------------- src/services/opencode-config.test.ts | 34 ++++++++++++++++++++++++++ src/services/opencode-config.ts | 25 +++++++++++++++++++ 5 files changed, 72 insertions(+), 72 deletions(-) delete mode 100644 src/config.test.ts diff --git a/src/cli.ts b/src/cli.ts index 97c32d2..f44caac 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,11 +4,12 @@ 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_FILE, SUPERMEMORY_API_KEY, getApiBaseUrl, isConfigured } from "./config.js"; import { SupermemoryClient } from "./services/client.js"; import { getTags } from "./services/tags.js"; import { stripJsoncComments } from "./services/jsonc.js"; import { + applyInstallDefaults, editOpenCodeConfig, editOpenCodeTuiConfig, readOpenCodeRegistration, @@ -409,6 +410,16 @@ function disableAutoCompactHook(): boolean { } } +function writeInstallDefaults(isExistingInstall: boolean): void { + const raw = isExistingInstall ? readFileSync(DEFAULT_CONFIG_FILE, "utf-8") : ""; + const next = applyInstallDefaults(raw, isExistingInstall); + if (next === null) { + console.warn(`⚠ Could not parse ${DEFAULT_CONFIG_FILE}; left it unchanged.`); + } else if (next !== raw) { + writeFileSync(DEFAULT_CONFIG_FILE, next); + } +} + interface InstallOptions { tui: boolean; disableAutoCompact: boolean; diff --git a/src/config.test.ts b/src/config.test.ts deleted file mode 100644 index b03a89c..0000000 --- a/src/config.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { applyInstallDefaults } from "./config.js"; - -const JSONC = `{ - // created per README - "apiKey": "sm_test", - "recallMode": "advisory" -} -`; - -describe("applyInstallDefaults", () => { - test("keeps comments and user settings in an existing jsonc config", () => { - const next = applyInstallDefaults(JSONC, true)!; - - expect(next).toContain("// created per README"); - expect(next).toContain('"recallMode": "advisory"'); - expect(next).toContain('"captureEveryNTurns": 3'); - }); - - test("leaves an existing capture cadence untouched", () => { - const content = `{ "captureEveryNTurns": 5 } // keep`; - expect(applyInstallDefaults(content, true)).toBe(content); - }); - - test("writes fresh-install defaults to an empty config", () => { - expect(JSON.parse(applyInstallDefaults("", false)!)).toEqual({ - recallMode: "direct", - captureEveryNTurns: 0, - }); - }); - - test("does not rewrite a config it cannot parse", () => { - expect(applyInstallDefaults(`{ "apiKey": `, true)).toBeNull(); - }); -}); diff --git a/src/config.ts b/src/config.ts index 252f18b..338b8ef 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,9 +1,8 @@ -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"; -import { applyEdits, modify, parse, type ParseError } from "jsonc-parser/lib/esm/main.js"; const CONFIG_DIR = join(homedir(), ".config", "opencode"); export { PLUGIN_VERSION } from "./version.js"; @@ -175,7 +174,6 @@ export function getApiBaseUrl(): string { /** The file the config was loaded from, or where a new one should be created. */ export const CONFIG_FILE = loadedConfigFile ?? CONFIG_FILES[1]; -const DEFAULT_CONFIG_FILE = CONFIG_FILE ?? join(CONFIG_DIR, "supermemory.json"); export const CONFIG = { similarityThreshold: fileConfig.similarityThreshold ?? DEFAULTS.similarityThreshold, @@ -220,35 +218,3 @@ export function getRecallConfig(): { mode: CONFIG.recallMode, }; } - -/** - * Adds install defaults to raw config content without rewriting the rest of - * the file, so comments in supermemory.jsonc survive. Returns null when the - * content is not valid JSONC and should be left alone. - */ -export function applyInstallDefaults(rawContent: string, isExistingInstall: boolean): string | null { - const content = rawContent.trim() === "" ? "{}\n" : rawContent; - const errors: ParseError[] = []; - const current = parse(content, errors, { allowTrailingComma: true }) as SupermemoryConfig | undefined; - if (errors.length > 0 || typeof current !== "object" || current === null || Array.isArray(current)) { - return null; - } - - const defaults: SupermemoryConfig = isExistingInstall - ? current.captureEveryNTurns === undefined ? { captureEveryNTurns: 3 } : {} - : { recallMode: "direct", captureEveryNTurns: 0 }; - - let next = content; - for (const [key, value] of Object.entries(defaults)) { - next = applyEdits(next, modify(next, [key], value, { - formattingOptions: { insertSpaces: true, tabSize: 2 }, - })); - } - return next; -} - -export function writeInstallDefaults(isExistingInstall: boolean): void { - const raw = existsSync(DEFAULT_CONFIG_FILE) ? readFileSync(DEFAULT_CONFIG_FILE, "utf-8") : ""; - const next = applyInstallDefaults(raw, isExistingInstall); - if (next !== null && next !== raw) writeFileSync(DEFAULT_CONFIG_FILE, next); -} diff --git a/src/services/opencode-config.test.ts b/src/services/opencode-config.test.ts index 79b1a48..40dea19 100644 --- a/src/services/opencode-config.test.ts +++ b/src/services/opencode-config.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { + applyInstallDefaults, editOpenCodeConfig, editOpenCodeTuiConfig, readOpenCodeRegistration, @@ -89,3 +90,36 @@ describe("OpenCode V1 TUI config editor", () => { expect(readOpenCodeTuiRegistration('{"plugin": ["other"]}')).toBe(false); }); }); + +describe("applyInstallDefaults", () => { + const jsonc = `{ + // created per README + "apiKey": "sm_test", + "recallMode": "advisory" +} +`; + + test("keeps comments and user settings in an existing jsonc config", () => { + const next = applyInstallDefaults(jsonc, true)!; + + expect(next).toContain("// created per README"); + expect(next).toContain('"recallMode": "advisory"'); + expect(next).toContain('"captureEveryNTurns": 3'); + }); + + test("leaves an existing capture cadence untouched", () => { + const content = `{ "captureEveryNTurns": 5 } // keep`; + expect(applyInstallDefaults(content, true)).toBe(content); + }); + + test("writes fresh-install defaults to an empty config", () => { + expect(JSON.parse(applyInstallDefaults("", false)!)).toEqual({ + recallMode: "direct", + captureEveryNTurns: 0, + }); + }); + + test("does not rewrite a config it cannot parse", () => { + expect(applyInstallDefaults(`{ "apiKey": `, true)).toBeNull(); + }); +}); diff --git a/src/services/opencode-config.ts b/src/services/opencode-config.ts index c486259..4ef2316 100644 --- a/src/services/opencode-config.ts +++ b/src/services/opencode-config.ts @@ -206,3 +206,28 @@ export function editOpenCodeConfig(rawContent: string): OpenCodeConfigEditResult warnings, }; } + +/** + * Adds installer defaults to supermemory.json(c) without rewriting the rest of + * the file, so user comments survive. Returns null for content that is not a + * valid JSONC object, which should be left for the user to fix. + */ +export function applyInstallDefaults(rawContent: string, isExistingInstall: boolean): string | null { + const content = rawContent.trim() === "" ? "{}\n" : rawContent; + let config: JsonObject; + try { + config = parseOpenCodeConfig(content); + } catch { + return null; + } + + const defaults: JsonObject = isExistingInstall + ? config.captureEveryNTurns === undefined ? { captureEveryNTurns: 3 } : {} + : { recallMode: "direct", captureEveryNTurns: 0 }; + + let next = content; + for (const [key, value] of Object.entries(defaults)) { + next = applyModification(next, [key], value); + } + return next; +}