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
16 changes: 14 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +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,
Expand Down Expand Up @@ -408,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;
Expand Down Expand Up @@ -535,7 +547,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;
Expand Down
26 changes: 7 additions & 19 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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";
Expand Down Expand Up @@ -118,22 +118,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;
Expand Down Expand Up @@ -172,8 +172,8 @@ export function getApiBaseUrl(): string {
return normalized;
}

export const CONFIG_FILE = CONFIG_FILES[1];
const DEFAULT_CONFIG_FILE = CONFIG_FILE ?? join(CONFIG_DIR, "supermemory.json");
/** The file the config was loaded from, or where a new one should be created. */
export const CONFIG_FILE = loadedConfigFile ?? CONFIG_FILES[1];

export const CONFIG = {
similarityThreshold: fileConfig.similarityThreshold ?? DEFAULTS.similarityThreshold,
Expand Down Expand Up @@ -218,15 +218,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));
}
34 changes: 34 additions & 0 deletions src/services/opencode-config.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";

import {
applyInstallDefaults,
editOpenCodeConfig,
editOpenCodeTuiConfig,
readOpenCodeRegistration,
Expand Down Expand Up @@ -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();
});
});
25 changes: 25 additions & 0 deletions src/services/opencode-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}